From f3f260272291d0353931041346fa399c9939bfb2 Mon Sep 17 00:00:00 2001 From: "V. David Zvenyach" Date: Fri, 2 Oct 2026 07:45:54 -0500 Subject: [PATCH 1/2] feat(budget): source_anomalies and account_category on budget accounts Supports the Tango API 5.7.0 budget-account contract change. `BudgetAccount` gains `account_category` and `source_anomalies`, typed by the new `BudgetSourceAnomaly`, `BudgetSourceAnomalySource` and `BudgetSourceAnomalyRow` interfaces with every property optional. `ListBudgetAccountsOptions` types `account_category`, `account_category__in` and `agency`. Re-vendors the contract for 5.7.0, regenerates the shape overlay, and baselines Federal Register documents as a not-yet-ported resource. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 + contracts/conformance_baseline.json | 1 + contracts/filter_shape_contract.json | 449 +++++++++++++++++++++- contracts/shape_coverage_baseline.json | 3 +- docs/API_REFERENCE.md | 31 +- scripts/check-filter-shape-conformance.ts | 2 + src/client.ts | 7 + src/models/BudgetAccount.ts | 42 ++ src/models/index.ts | 2 +- src/shapes/generatedOverlay.ts | 36 ++ tests/unit/client.parity.test.ts | 38 ++ tests/unit/shapes.overlay.test.ts | 7 + 12 files changed, 607 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9cb43..11459dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This project follows [Semantic Versioning](https://semver.org/). ### Added +- **Budget account `source_anomalies` and `account_category`** (Tango API 5.7.0). `BudgetAccount` gains `account_category` (`"budgetary"` or `"credit_financing"` today, typed as an open string) and `source_anomalies`, a list that is `[]` when the row is clean. Each entry is a new `BudgetSourceAnomaly` (with `BudgetSourceAnomalySource` and `BudgetSourceAnomalyRow` for its upstream rows), and every property on it is optional. `action` is `"capped"` when a served value was clamped to its bound and `"flagged"` when it is served as reported; `code` is an open string. `ListBudgetAccountsOptions` types the new `account_category` and `account_category__in` filters, plus the `agency` filter the API already accepted. There is no anomaly filter. The default ordering now puts accounts with a null `enacted_ba` last, which is documented on the options interface and in `docs/API_REFERENCE.md`. + - **Boards-of-contract-appeals decisions** (Tango API 4.26.0). `listContractAppeals(options)` and `getContractAppeal(uuid, options)` over `/api/contract_appeals/`, with every filter the API accepts declared as a typed option on `ListContractAppealsOptions` (`search`, `board`, `docket`, `appellant`, `judge`, `decision_type`, the `decision_date_after` / `_before` pair, `listed`, `document_id`, `ordering`), the new `ContractAppealRecord` return type, and a registered `ContractAppeal` shape schema so the typed shape API resolves the resource's fields. These are Contract Disputes Act decisions from the CBCA (civilian) and the ASBCA (defense) — a dispute under an existing contract, not a challenge to an award. Bid protests remain the separate `listProtests()` resource, and the two do not overlap. @@ -28,6 +30,7 @@ This project follows [Semantic Versioning](https://semver.org/). ### Changed +- Re-vendored the contract for Tango API 5.7.0 and regenerated the overlay, so budget accounts, opportunities, notices, exclusions and SBIR shapes resolve the fields the API now serves. Federal Register documents are in the contract without an SDK method yet, so the resource is baselined as a tracked gap in both conformance baselines. - Re-vendored `contracts/filter_shape_contract.json` (schema_version 2, 48 resources) and regenerated `src/shapes/generatedOverlay.ts` from it — 359 fields across 25 containers, 73 nested schemas. - Re-vendored the contract for Tango API 5.1.0 and regenerated the overlay, which now merges a model's expand when two resources embed it instead of letting the narrower copy win. eBuy requests are in the contract without an SDK method yet, so it is baselined as a tracked gap. - Baselined 14 reverse-shape-coverage gaps in `contracts/shape_coverage_baseline.json`, matching tango-python. All 14 are the nested sub-resource routes above, which reuse the parent resource's model rather than carrying one of their own; none is SLED, and none is a regression — they became visible only with the re-vendored contract. diff --git a/contracts/conformance_baseline.json b/contracts/conformance_baseline.json index a63dbeb..6a0c05b 100644 --- a/contracts/conformance_baseline.json +++ b/contracts/conformance_baseline.json @@ -4,6 +4,7 @@ "unimplemented_resources": [ "ebuy/requests", "events", + "federal_register", "news" ] } diff --git a/contracts/filter_shape_contract.json b/contracts/filter_shape_contract.json index 0e47b14..b8b8cac 100644 --- a/contracts/filter_shape_contract.json +++ b/contracts/filter_shape_contract.json @@ -1,6 +1,6 @@ { "meta": { - "api_version": "5.1.0", + "api_version": "5.7.0", "description": "Canonical API filter/shape contract. Downstream consumers (SDK, MCP) should validate their conformance against this manifest.", "generated_from": "scripts/filter_shape_conformance.py", "schema_version": 2 @@ -1628,6 +1628,8 @@ "resource_key": "budget/accounts", "runtime": { "filter_params": [ + "account_category", + "account_category__in", "account_title__icontains", "actual_vs_requested_contract", "actual_vs_requested_contract__gte", @@ -1635,6 +1637,7 @@ "actual_vs_requested_contract_capped", "actual_vs_requested_contract_capped__gte", "actual_vs_requested_contract_capped__lte", + "agency", "agency_code", "agency_code__in", "apportioned", @@ -1726,6 +1729,15 @@ "unobligated_pct__lte" ], "filter_params_detail": { + "account_category": { + "filter_class": "CharFilter", + "type": "string" + }, + "account_category__in": { + "filter_class": "CharInFilter", + "lookup": "in", + "type": "string" + }, "account_title__icontains": { "filter_class": "CharFilter", "lookup": "icontains", @@ -1759,6 +1771,10 @@ "lookup": "lte", "type": "number" }, + "agency": { + "filter_class": "OrganizationScopeAgencyFilter", + "type": "string" + }, "agency_code": { "filter_class": "CharFilter", "type": "string" @@ -2240,9 +2256,22 @@ "subaccount_code", "subfunction_code" ] + }, + "organization": { + "expands": {}, + "fields": [ + "agency_code", + "agency_name", + "department_code", + "department_name", + "office_code", + "office_name", + "organization_id" + ] } }, "fields": [ + "account_category", "account_narrative_excerpt", "account_title", "actual_vs_requested_contract", @@ -2299,6 +2328,7 @@ "obligated_total", "obligated_yoy_pct", "on_off_budget", + "organization_id", "outlayed_to_obligated_pct", "outlayed_to_obligated_pct_capped", "outlayed_to_obligated_pct_capped_flag", @@ -2306,6 +2336,7 @@ "requested_ba", "requested_contractual_services", "requested_personnel_share", + "source_anomalies", "subfunction_code", "top_contract_recipients", "top_grant_recipients", @@ -2315,6 +2346,7 @@ ] }, "shape_flat_paths": [ + "account_category", "account_narrative_excerpt", "account_title", "actual_vs_requested_contract", @@ -2407,6 +2439,15 @@ "obligated_total", "obligated_yoy_pct", "on_off_budget", + "organization", + "organization.agency_code", + "organization.agency_name", + "organization.department_code", + "organization.department_name", + "organization.office_code", + "organization.office_name", + "organization.organization_id", + "organization_id", "outlayed_to_obligated_pct", "outlayed_to_obligated_pct_capped", "outlayed_to_obligated_pct_capped_flag", @@ -2414,6 +2455,7 @@ "requested_ba", "requested_contractual_services", "requested_personnel_share", + "source_anomalies", "subfunction_code", "top_contract_recipients", "top_grant_recipients", @@ -4174,6 +4216,7 @@ "resource_key": "ebuy/requests", "runtime": { "filter_params": [ + "agency", "buyer_agency", "close_date_after", "close_date_before", @@ -4189,6 +4232,10 @@ "status" ], "filter_params_detail": { + "agency": { + "filter_class": "OrganizationScopeAgencyFilter", + "type": "string" + }, "buyer_agency": { "filter_class": "BaseSmartFilter", "type": "string" @@ -4234,7 +4281,7 @@ "type": "string" }, "search": { - "filter_class": "BaseSmartFilter", + "filter_class": "EbuyRequestSearchFilter", "type": "string" }, "sin": { @@ -7363,6 +7410,7 @@ "activate_date_after", "activate_date_before", "active", + "agency", "cage_code", "classification_type", "delisted", @@ -7394,6 +7442,10 @@ "filter_class": "BooleanFilter", "type": "boolean" }, + "agency": { + "filter_class": "OrganizationScopeAgencyFilter", + "type": "string" + }, "cage_code": { "filter_class": "BaseSmartFilter", "type": "string" @@ -7474,7 +7526,20 @@ "max_page_size": 100 }, "shape": { - "expands": {}, + "expands": { + "organization": { + "expands": {}, + "fields": [ + "agency_code", + "agency_name", + "department_code", + "department_name", + "office_code", + "office_name", + "organization_id" + ] + } + }, "fields": [ "activate_date", "additional_comments", @@ -7500,6 +7565,7 @@ "middle_name", "more_locations", "npi", + "organization_id", "prefix", "primary_address", "references", @@ -7542,6 +7608,15 @@ "middle_name", "more_locations", "npi", + "organization", + "organization.agency_code", + "organization.agency_name", + "organization.department_code", + "organization.department_name", + "organization.office_code", + "organization.office_name", + "organization.organization_id", + "organization_id", "prefix", "primary_address", "references", @@ -7575,6 +7650,230 @@ "show_shapes" ] }, + "federal_register": { + "basename": "federalregisterdocument", + "docs_file": null, + "docs_params": [], + "prefix": "federal_register", + "resource_key": "federal_register", + "runtime": { + "filter_params": [ + "agency", + "cfr_part", + "cfr_title", + "comments_close_on_after", + "comments_close_on_before", + "comments_open", + "document_number", + "effective_on_after", + "effective_on_before", + "executive_order_number", + "fr_agency", + "publication_date_after", + "publication_date_before", + "rin", + "search", + "significant", + "type" + ], + "filter_params_detail": { + "agency": { + "filter_class": "FederalRegisterAgencyFilter", + "type": "string" + }, + "cfr_part": { + "filter_class": "CharFilter", + "type": "string" + }, + "cfr_title": { + "filter_class": "CharFilter", + "type": "string" + }, + "comments_close_on_after": { + "filter_class": "DateFilter", + "lookup": "gte", + "type": "date" + }, + "comments_close_on_before": { + "filter_class": "DateFilter", + "lookup": "lte", + "type": "date" + }, + "comments_open": { + "filter_class": "BooleanFilter", + "type": "boolean" + }, + "document_number": { + "filter_class": "IdListFilter", + "type": "string" + }, + "effective_on_after": { + "filter_class": "DateFilter", + "lookup": "gte", + "type": "date" + }, + "effective_on_before": { + "filter_class": "DateFilter", + "lookup": "lte", + "type": "date" + }, + "executive_order_number": { + "filter_class": "CharFilter", + "type": "string" + }, + "fr_agency": { + "filter_class": "JsonbContainsFilter", + "type": "string" + }, + "publication_date_after": { + "filter_class": "DateFilter", + "lookup": "gte", + "type": "date" + }, + "publication_date_before": { + "filter_class": "DateFilter", + "lookup": "lte", + "type": "date" + }, + "rin": { + "filter_class": "JsonbContainsFilter", + "type": "string" + }, + "search": { + "filter_class": "FederalRegisterSearchFilter", + "type": "string" + }, + "significant": { + "filter_class": "BooleanFilter", + "type": "boolean" + }, + "type": { + "filter_class": "DocumentTypeFilter", + "type": "string" + } + }, + "ordering_aliases": [], + "ordering_fields": [ + "comments_close_on", + "document_number", + "effective_on", + "publication_date", + "rank" + ], + "pagination": { + "class": "CachedPageNumberPagination", + "max_page_size": 100 + }, + "shape": { + "expands": {}, + "fields": [ + "abstract", + "action", + "agencies", + "body_html_url", + "cfr_references", + "citation", + "comment_url", + "comments_close_on", + "correction_of", + "corrections", + "dates", + "disposition_notes", + "docket_ids", + "dockets", + "document_number", + "effective_on", + "end_page", + "executive_order_notes", + "executive_order_number", + "html_url", + "not_received_for_publication", + "page_length", + "pdf_url", + "presidential_document_number", + "proclamation_number", + "public_inspection_pdf_url", + "publication_date", + "raw_text_url", + "regulation_id_number_info", + "regulation_id_numbers", + "regulations_dot_gov_info", + "regulations_dot_gov_url", + "significant", + "signing_date", + "start_page", + "subtype", + "title", + "toc_doc", + "toc_subject", + "topics", + "type", + "uuid", + "volume" + ] + }, + "shape_flat_paths": [ + "abstract", + "action", + "agencies", + "body_html_url", + "cfr_references", + "citation", + "comment_url", + "comments_close_on", + "correction_of", + "corrections", + "dates", + "disposition_notes", + "docket_ids", + "dockets", + "document_number", + "effective_on", + "end_page", + "executive_order_notes", + "executive_order_number", + "html_url", + "not_received_for_publication", + "page_length", + "pdf_url", + "presidential_document_number", + "proclamation_number", + "public_inspection_pdf_url", + "publication_date", + "raw_text_url", + "regulation_id_number_info", + "regulation_id_numbers", + "regulations_dot_gov_info", + "regulations_dot_gov_url", + "significant", + "signing_date", + "start_page", + "subtype", + "title", + "toc_doc", + "toc_subject", + "topics", + "type", + "uuid", + "volume" + ], + "shape_gated_by_level": {}, + "shape_supported": true, + "shape_tier_required": null, + "viewset": "federal_register.views.FederalRegisterDocumentViewSet" + }, + "swagger_has_key": true, + "swagger_params": [ + "flat", + "flat_lists", + "joiner", + "limit", + "ordering", + "page", + "shape", + "show_shapes" + ] + }, "forecasts": { "basename": "forecast", "docs_file": null, @@ -10472,6 +10771,7 @@ "resource_key": "itdashboard", "runtime": { "filter_params": [ + "agency", "agency_code", "agency_name", "cio_rating", @@ -10484,6 +10784,10 @@ "updated_time_before" ], "filter_params_detail": { + "agency": { + "filter_class": "OrganizationScopeAgencyFilter", + "type": "string" + }, "agency_code": { "filter_class": "NumberFilter", "type": "number" @@ -11169,7 +11473,11 @@ "archive", "attachment_count", "attachments", + "award_amount", + "award_date", "award_number", + "awardee", + "awardee_uei", "description", "last_updated", "meta", @@ -11211,7 +11519,11 @@ "attachments.resource_id", "attachments.type", "attachments.url", + "award_amount", + "award_date", "award_number", + "awardee", + "awardee_uei", "description", "last_updated", "meta", @@ -11401,6 +11713,8 @@ "filter_params": [ "active", "agency", + "awarded", + "awardee_uei", "first_notice_date_after", "first_notice_date_before", "last_notice_date_after", @@ -11425,6 +11739,14 @@ "filter_class": "PerformantAgencyFilter", "type": "string" }, + "awarded": { + "filter_class": "BooleanFilter", + "type": "boolean" + }, + "awardee_uei": { + "filter_class": "AwardeeUeiFilter", + "type": "string" + }, "first_notice_date_after": { "filter_class": "DateFromToRangeFilter", "lookup": "gte", @@ -11526,6 +11848,18 @@ "url" ] }, + "awards": { + "expands": {}, + "fields": [ + "award_amount", + "award_date", + "award_number", + "awardee", + "awardee_uei", + "notice_id", + "opportunity_id" + ] + }, "department": { "expands": {}, "fields": [ @@ -11632,7 +11966,13 @@ "agency_id", "archive_date", "attachments", + "award_amount", + "award_count", + "award_date", "award_number", + "awarded", + "awardee", + "awardee_uei", "department", "department_id", "description", @@ -11654,6 +11994,7 @@ "set_aside", "snippet", "solicitation_number", + "solicitation_opportunity_id", "title" ] }, @@ -11677,7 +12018,21 @@ "attachments.resource_id", "attachments.type", "attachments.url", + "award_amount", + "award_count", + "award_date", "award_number", + "awarded", + "awardee", + "awardee_uei", + "awards", + "awards.award_amount", + "awards.award_date", + "awards.award_number", + "awards.awardee", + "awards.awardee_uei", + "awards.notice_id", + "awards.opportunity_id", "department", "department.abbreviation", "department.cgac", @@ -11748,6 +12103,7 @@ "set_aside.description", "snippet", "solicitation_number", + "solicitation_opportunity_id", "title" ], "shape_gated_by_level": { @@ -11765,6 +12121,8 @@ "swagger_params": [ "active", "agency", + "awarded", + "awardee_uei", "first_notice_date_after", "first_notice_date_before", "flat", @@ -13598,6 +13956,18 @@ "s3_key" ] }, + "organization": { + "expands": {}, + "fields": [ + "agency_code", + "agency_name", + "department_code", + "department_name", + "office_code", + "office_name", + "organization_id" + ] + }, "topics": { "expands": {}, "fields": [ @@ -13642,6 +14012,14 @@ "documents.n_pages", "documents.s3_key", "end_date", + "organization", + "organization.agency_code", + "organization.agency_name", + "organization.department_code", + "organization.department_name", + "organization.office_code", + "organization.office_name", + "organization.organization_id", "out_of_cycle", "program", "sol_download_url", @@ -13681,6 +14059,7 @@ "agency", "close_date_after", "close_date_before", + "cycle_name", "doc_source", "open_date_after", "open_date_before", @@ -13703,8 +14082,7 @@ "type": "choice" }, "agency": { - "filter_class": "CharFilter", - "lookup": "icontains", + "filter_class": "OrganizationScopeAgencyFilter", "type": "string" }, "close_date_after": { @@ -13717,6 +14095,10 @@ "lookup": "lte", "type": "date" }, + "cycle_name": { + "filter_class": "CharFilter", + "type": "string" + }, "doc_source": { "filter_class": "CharFilter", "lookup": "iexact", @@ -13794,6 +14176,18 @@ "title" ] }, + "organization": { + "expands": {}, + "fields": [ + "agency_code", + "agency_name", + "department_code", + "department_name", + "office_code", + "office_name", + "organization_id" + ] + }, "solicitation": { "expands": {}, "fields": [ @@ -13814,6 +14208,7 @@ "activity", "agency", "close_date", + "cycle_name", "description", "doc_source", "due_dates_text", @@ -13836,6 +14231,7 @@ "activity", "agency", "close_date", + "cycle_name", "description", "doc_source", "due_dates_text", @@ -13852,6 +14248,14 @@ "opportunity.response_deadline", "opportunity.solicitation_number", "opportunity.title", + "organization", + "organization.agency_code", + "organization.agency_name", + "organization.department_code", + "organization.department_name", + "organization.office_code", + "organization.office_name", + "organization.organization_id", "release_date", "solicitation", "solicitation.cycle_name", @@ -15485,6 +15889,18 @@ "url" ] }, + "awards": { + "expands": {}, + "fields": [ + "award_amount", + "award_date", + "award_number", + "awardee", + "awardee_uei", + "notice_id", + "opportunity_id" + ] + }, "department": { "expands": {}, "fields": [ @@ -15591,7 +16007,13 @@ "agency_id", "archive_date", "attachments", + "award_amount", + "award_count", + "award_date", "award_number", + "awarded", + "awardee", + "awardee_uei", "department", "department_id", "description", @@ -15613,6 +16035,7 @@ "set_aside", "snippet", "solicitation_number", + "solicitation_opportunity_id", "title" ] }, @@ -16088,7 +16511,21 @@ "opportunity.attachments.resource_id", "opportunity.attachments.type", "opportunity.attachments.url", + "opportunity.award_amount", + "opportunity.award_count", + "opportunity.award_date", "opportunity.award_number", + "opportunity.awarded", + "opportunity.awardee", + "opportunity.awardee_uei", + "opportunity.awards", + "opportunity.awards.award_amount", + "opportunity.awards.award_date", + "opportunity.awards.award_number", + "opportunity.awards.awardee", + "opportunity.awards.awardee_uei", + "opportunity.awards.notice_id", + "opportunity.awards.opportunity_id", "opportunity.department", "opportunity.department.abbreviation", "opportunity.department.cgac", @@ -16159,6 +16596,7 @@ "opportunity.set_aside.description", "opportunity.snippet", "opportunity.solicitation_number", + "opportunity.solicitation_opportunity_id", "opportunity.title", "opportunity_id", "order_count", @@ -16993,6 +17431,7 @@ "entities_subawards", "events", "exclusions", + "federal_register", "forecasts", "grants", "gsa_elibrary_contracts", diff --git a/contracts/shape_coverage_baseline.json b/contracts/shape_coverage_baseline.json index 0d20bc4..3f45d57 100644 --- a/contracts/shape_coverage_baseline.json +++ b/contracts/shape_coverage_baseline.json @@ -1,6 +1,6 @@ { "description": "Known reverse shape-coverage gaps (Tango exposes, SDK schema lacks), accepted as a tracked backlog. check-shape-coverage.ts fails only on gaps NOT listed here. Burn down and regenerate with --update-baseline.", - "count": 15, + "count": 16, "known_gaps": [ "unmapped_resource|agencies_contracts_awarding|(root)|(no model mapped)", "unmapped_resource|agencies_contracts_funding|(root)|(no model mapped)", @@ -12,6 +12,7 @@ "unmapped_resource|entities_otas|(root)|(no model mapped)", "unmapped_resource|entities_otidvs|(root)|(no model mapped)", "unmapped_resource|entities_subawards|(root)|(no model mapped)", + "unmapped_resource|federal_register|(root)|(no model mapped)", "unmapped_resource|idvs_awards|(root)|(no model mapped)", "unmapped_resource|idvs_idvs|(root)|(no model mapped)", "unmapped_resource|idvs_lcats|(root)|(no model mapped)", diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index ddbb4e7..ad0e8e9 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -487,16 +487,33 @@ const accounts = await client.listBudgetAccounts({ The range filters use the API's **dunder wire names** (double underscore, e.g. `fiscal_year__gte`) — these are passed through verbatim. A representative sample: -| Filter family | Example params | -| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| Identity / categorical | `federal_account_symbol`, `fiscal_year`, `agency_code__in`, `bureau_name__icontains`, `bea_category`, `subfunction_code`, `account_title__icontains` | -| Lifecycle amounts | `requested_ba__gte`, `enacted_ba__lte`, `apportioned__gte`, `obligated_total__gte`, `outlayed_total__lte`, `unobligated_balance__gte` | -| Contract / assistance breakdowns | `contract_obligated__gte`, `assistance_outlayed__lte`, `contract_share_of_obligated_capped__gte` | -| Ratios | `obligated_to_apportioned_pct__gte`, `apportioned_to_enacted_pct_capped__lte`, `outlayed_to_obligated_pct__gte`, `unobligated_pct__gte` | -| Trends | `enacted_ba_yoy_pct__gte`, `obligated_yoy_pct__lte`, `enacted_ba_5yr_cagr__gte`, `ba_growth_next_year_pct__gte`, `actual_vs_requested_contract__gte` | +| Filter family | Example params | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Identity / categorical | `federal_account_symbol`, `fiscal_year`, `agency`, `agency_code__in`, `bureau_name__icontains`, `bea_category`, `account_category__in`, `subfunction_code`, `account_title__icontains` | +| Lifecycle amounts | `requested_ba__gte`, `enacted_ba__lte`, `apportioned__gte`, `obligated_total__gte`, `outlayed_total__lte`, `unobligated_balance__gte` | +| Contract / assistance breakdowns | `contract_obligated__gte`, `assistance_outlayed__lte`, `contract_share_of_obligated_capped__gte` | +| Ratios | `obligated_to_apportioned_pct__gte`, `apportioned_to_enacted_pct_capped__lte`, `outlayed_to_obligated_pct__gte`, `unobligated_pct__gte` | +| Trends | `enacted_ba_yoy_pct__gte`, `obligated_yoy_pct__lte`, `enacted_ba_5yr_cagr__gte`, `ba_growth_next_year_pct__gte`, `actual_vs_requested_contract__gte` | See `ListBudgetAccountsOptions` in `src/client.ts` for the complete list — every filter is a typed, autocompleted option. Any of the numeric fields is a valid `ordering` target (`ordering: "-unobligated_balance"` ranks by largest headroom first), and `search` covers account title, agency name, and bureau name. +With no `ordering`, rows come back newest fiscal year first, then by `enacted_ba` descending, with accounts that have no `enacted_ba` last. + +**Account category.** Each row carries `account_category` (`"budgetary"` or `"credit_financing"` today; treat it as an open string), and `account_category` / `account_category__in` filter on it. +For example, `account_category: "budgetary"` leaves out credit financing accounts. + +**Source anomalies.** Each row carries `source_anomalies`, a list typed as `BudgetSourceAnomaly[]` that is `[]` when the row is clean. +An entry records a source-data inconsistency the API found and what it did: `action: "capped"` means a served value was clamped to its bound (`reported_value` is what the source said, `served_value` is what the row carries), and `action: "flagged"` means the value is served as reported and only annotated. +`code` is an open string (`contract_exceeds_obligations`, `assistance_exceeds_obligations` and `contract_without_obligations` today), `affected_fields` names the fields the anomaly touches, and `source` points at the upstream rows behind it. +Every property on an entry is optional. +There is no filter on anomalies, so check the list client-side: + +```ts +import type { BudgetAccount } from "@makegov/tango-node/models"; + +const res = await client.listBudgetAccounts({ fiscal_year: 2025, account_category: "budgetary" }); +const capped = (res.results as BudgetAccount[]).filter((row) => row.source_anomalies?.some((a) => a.action === "capped")); +``` **Legacy aliases.** Three pre-1.2 option names are kept and remapped to the params the API actually understands: `fiscal_year_gte` → `fiscal_year__gte`, `fiscal_year_lte` → `fiscal_year__lte`, and `account_title` → `account_title__icontains`. An explicitly passed dunder param wins over its alias. diff --git a/scripts/check-filter-shape-conformance.ts b/scripts/check-filter-shape-conformance.ts index b5bf2a5..45740ea 100644 --- a/scripts/check-filter-shape-conformance.ts +++ b/scripts/check-filter-shape-conformance.ts @@ -71,6 +71,8 @@ export const RESOURCE_TO_METHOD: Record = { contract_appeals: "listContractAppeals", // eBuy requests are published by the API but not yet ported — baselined as a tracked gap. "ebuy/requests": null, + // Federal Register documents are published by the API but not yet ported — baselined as a tracked gap. + federal_register: null, psc: "listPsc", mas_sins: "listMasSins", departments: "listDepartments", diff --git a/src/client.ts b/src/client.ts index 59fad05..40c416b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -414,6 +414,7 @@ export interface ListGrantsOptions extends ListOptionsBase { * Budget account list options — matches `tango_python.TangoClient.list_budget_accounts`. * * Every numeric lifecycle/ratio field exposes an exact / `__gte` / `__lte` triplet, and any of them is a valid `ordering` target (e.g. `ordering: "-unobligated_balance"` ranks by largest headroom first). + * With no `ordering`, rows come back newest fiscal year first, then by `enacted_ba` descending with null `enacted_ba` last. */ export interface ListBudgetAccountsOptions extends ListOptionsBase { // Identity / categorical filters (`__in` variants take a comma-separated list) @@ -440,6 +441,12 @@ export interface ListBudgetAccountsOptions extends ListOptionsBase { on_off_budget?: string; subfunction_code?: string; subfunction_code__in?: string; + /** Account category, e.g. `"budgetary"` or `"credit_financing"`. */ + account_category?: string; + /** Comma-separated list of account categories. */ + account_category__in?: string; + /** Agency by name, abbreviation, or code; a department rolls up across its bureaus. Multi-value OR via `|`. */ + agency?: string; // President's-budget requested BA requested_ba?: number | string; diff --git a/src/models/BudgetAccount.ts b/src/models/BudgetAccount.ts index 69b6971..48348cf 100644 --- a/src/models/BudgetAccount.ts +++ b/src/models/BudgetAccount.ts @@ -1,3 +1,41 @@ +/** One FY-specific File C source row behind a {@link BudgetSourceAnomaly}. Every property is optional. */ +export interface BudgetSourceAnomalyRow { + fiscal_period?: number | string | null; + piid?: string | null; + parent_piid?: string | null; + tas?: string | null; + reporting_agency_id?: string | null; + transaction_obligated_amount?: number | string | null; + file_c_source?: string | null; +} + +/** Where a {@link BudgetSourceAnomaly} was observed in the upstream spending data. */ +export interface BudgetSourceAnomalySource { + dataset?: string | null; + fiscal_year?: number | null; + rows?: BudgetSourceAnomalyRow[] | null; +} + +/** + * A source-data inconsistency the API detected on a budget account row, and what it did about it. + * + * `action` is `"capped"` when a served value was clamped to its bound (`reported_value` is what the source said, `served_value` is what the row carries), or `"flagged"` when the value is served as reported and only annotated. + * `code` is an open string: today's codes are `contract_exceeds_obligations`, `assistance_exceeds_obligations`, and `contract_without_obligations`, and more may be added without notice. + * Every property is optional and may be null. + */ +export interface BudgetSourceAnomaly { + code?: string | null; + field?: string | null; + bound_field?: string | null; + action?: "capped" | "flagged" | (string & {}) | null; + reported_value?: number | string | null; + served_value?: number | string | null; + likely_cause?: string | null; + affected_fields?: string[] | null; + message?: string | null; + source?: BudgetSourceAnomalySource | null; +} + /** * Federal account x fiscal year budget rollup. * @@ -22,6 +60,8 @@ export interface BudgetAccount { bea_category?: string | null; on_off_budget?: string | null; subfunction_code?: string | null; + /** Account category: `"budgetary"` or `"credit_financing"` today. Treat as an open string. */ + account_category?: string | null; // Lifecycle requested_ba?: number | null; @@ -47,6 +87,8 @@ export interface BudgetAccount { assistance_share_of_obligated?: number | null; assistance_share_of_obligated_capped?: number | null; assistance_share_capped_flag?: boolean; + /** Source-data inconsistencies detected on this row; `[]` when the row is clean. */ + source_anomalies?: BudgetSourceAnomaly[] | null; // Forward-look next_year_requested_ba?: number | null; diff --git a/src/models/index.ts b/src/models/index.ts index af66121..c58cdae 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,5 +1,5 @@ export type { Agency } from "./Agency.js"; -export type { BudgetAccount } from "./BudgetAccount.js"; +export type { BudgetAccount, BudgetSourceAnomaly, BudgetSourceAnomalyRow, BudgetSourceAnomalySource } from "./BudgetAccount.js"; export type { Contract, OrganizationOfficePayload, diff --git a/src/shapes/generatedOverlay.ts b/src/shapes/generatedOverlay.ts index 0f90a9c..3ebc377 100644 --- a/src/shapes/generatedOverlay.ts +++ b/src/shapes/generatedOverlay.ts @@ -144,6 +144,15 @@ export const GENERATED_NESTED: Record = { total_contract_value: f("total_contract_value", "Decimal"), transactions: f("transactions", "str"), }, + Awards2: { + award_amount: f("award_amount", "Decimal"), + award_date: f("award_date", "date"), + award_number: f("award_number", "str"), + awardee: f("awardee", "str"), + awardee_uei: f("awardee_uei", "str"), + notice_id: f("notice_id", "str"), + opportunity_id: f("opportunity_id", "str"), + }, BudgetAppropriation: { cgac: f("cgac", "int"), fiscal_year: f("fiscal_year", "int"), @@ -657,6 +666,7 @@ export const GENERATED_OVERLAY: Record = { organization_id: f("organization_id", "str"), }, BudgetAccount: { + account_category: f("account_category", "str"), account_narrative_excerpt: f("account_narrative_excerpt", "str"), account_title: f("account_title", "str"), actual_vs_requested_contract: f("actual_vs_requested_contract", "str"), @@ -715,6 +725,8 @@ export const GENERATED_OVERLAY: Record = { obligated_total: f("obligated_total", "str"), obligated_yoy_pct: f("obligated_yoy_pct", "str"), on_off_budget: f("on_off_budget", "str"), + organization: f("organization", "dict", false, "AwardingOffice"), + organization_id: f("organization_id", "str"), outlayed_to_obligated_pct: f("outlayed_to_obligated_pct", "str"), outlayed_to_obligated_pct_capped: f("outlayed_to_obligated_pct_capped", "str"), outlayed_to_obligated_pct_capped_flag: f("outlayed_to_obligated_pct_capped_flag", "str"), @@ -722,6 +734,7 @@ export const GENERATED_OVERLAY: Record = { requested_ba: f("requested_ba", "str"), requested_contractual_services: f("requested_contractual_services", "str"), requested_personnel_share: f("requested_personnel_share", "str"), + source_anomalies: f("source_anomalies", "str"), subfunction_code: f("subfunction_code", "str"), top_contract_recipients: f("top_contract_recipients", "str"), top_grant_recipients: f("top_grant_recipients", "str"), @@ -801,6 +814,10 @@ export const GENERATED_OVERLAY: Record = { uuid: f("uuid", "str"), workforce: f("workforce", "dict", false, "Workforce"), }, + Exclusion: { + organization: f("organization", "dict", false, "AwardingOffice"), + organization_id: f("organization_id", "str"), + }, Forecast: { created: f("created", "datetime"), display: f("display", "dict", false, "Display"), @@ -903,6 +920,10 @@ export const GENERATED_OVERLAY: Record = { address: f("address", "dict", false, "Address"), archive: f("archive", "dict", false, "Archive"), attachments: f("attachments", "dict", true, "Attachments2"), + award_amount: f("award_amount", "Decimal"), + award_date: f("award_date", "date"), + awardee: f("awardee", "str"), + awardee_uei: f("awardee_uei", "str"), meta: f("meta", "dict", false, "Meta"), office: f("office", "dict", false, "AwardingOffice"), opportunity: f("opportunity", "dict", false, "Opportunity2"), @@ -967,6 +988,13 @@ export const GENERATED_OVERLAY: Record = { agency_id: f("agency_id", "str"), archive_date: f("archive_date", "date"), attachments: f("attachments", "dict", true, "Attachments5"), + award_amount: f("award_amount", "Decimal"), + award_count: f("award_count", "int"), + award_date: f("award_date", "date"), + awarded: f("awarded", "str"), + awardee: f("awardee", "str"), + awardee_uei: f("awardee_uei", "str"), + awards: f("awards", "dict", false, "Awards2"), department: f("department", "dict", false, "Department3"), department_id: f("department_id", "str"), latest_notice: f("latest_notice", "dict", false, "LatestNotice"), @@ -978,6 +1006,7 @@ export const GENERATED_OVERLAY: Record = { secondary_contact: f("secondary_contact", "dict", false, "PrimaryContact"), set_aside: f("set_aside", "dict", false, "CodeDescription"), snippet: f("snippet", "str"), + solicitation_opportunity_id: f("solicitation_opportunity_id", "str"), }, Organization: { aac_code: f("aac_code", "str"), @@ -1046,6 +1075,13 @@ export const GENERATED_OVERLAY: Record = { outcome_reason: f("outcome_reason", "str"), size_standard: f("size_standard", "str"), }, + SbirSolicitation: { + organization: f("organization", "dict", false, "AwardingOffice"), + }, + SbirTopic: { + cycle_name: f("cycle_name", "str"), + organization: f("organization", "dict", false, "AwardingOffice"), + }, Vehicle: { name: f("name", "str"), }, diff --git a/tests/unit/client.parity.test.ts b/tests/unit/client.parity.test.ts index e0b60b2..5741c31 100644 --- a/tests/unit/client.parity.test.ts +++ b/tests/unit/client.parity.test.ts @@ -6,6 +6,7 @@ */ import { TangoClient } from "../../src/client.js"; +import type { BudgetAccount } from "../../src/models/index.js"; type RecordedCall = { url: string; init?: RequestInit | undefined }; @@ -335,6 +336,43 @@ describe("TangoClient — filter-surface catch-up", () => { expect(calls[0].url).not.toContain("2021"); }); + it("listBudgetAccounts sends account_category, account_category__in and agency", async () => { + const { client, calls } = makeClient(); + await client.listBudgetAccounts({ account_category: "budgetary", account_category__in: "budgetary,credit_financing", agency: "DOD" }); + const params = new URL(calls[0].url).searchParams; + expect(params.get("account_category")).toBe("budgetary"); + expect(params.get("account_category__in")).toBe("budgetary,credit_financing"); + expect(params.get("agency")).toBe("DOD"); + }); + + it("listBudgetAccounts returns source_anomalies as served", async () => { + const anomaly = { + code: "contract_exceeds_obligations", + field: "contract_obligated", + bound_field: "obligated_total", + action: "capped", + reported_value: 1200, + served_value: 1000, + affected_fields: ["contract_share_of_obligated"], + source: { dataset: "file_c", fiscal_year: 2025, rows: [{ fiscal_period: 6, piid: "ABC123", transaction_obligated_amount: 1200 }] }, + }; + const { client } = makeClient({ + count: 2, + next: null, + previous: null, + results: [ + { federal_account_symbol: "097-0100", account_category: "budgetary", source_anomalies: [anomaly] }, + { federal_account_symbol: "097-4000", account_category: "credit_financing", source_anomalies: [] }, + ], + }); + const resp = await client.listBudgetAccounts(); + const rows = resp.results as BudgetAccount[]; + expect(rows[0].source_anomalies).toEqual([anomaly]); + expect(rows[0].source_anomalies?.[0].source?.rows?.[0].piid).toBe("ABC123"); + expect(rows[1].source_anomalies).toEqual([]); + expect(rows[1].account_category).toBe("credit_financing"); + }); + it("listNaics sends the employee_limit filters", async () => { const { client, calls } = makeClient(); await client.listNaics({ employee_limit: 500, employee_limit_gte: 100 }); diff --git a/tests/unit/shapes.overlay.test.ts b/tests/unit/shapes.overlay.test.ts index 18be19a..7832e3a 100644 --- a/tests/unit/shapes.overlay.test.ts +++ b/tests/unit/shapes.overlay.test.ts @@ -59,6 +59,13 @@ describe("Entity relationships shape coverage", () => { } }); + it("BudgetAccount resolves source_anomalies and account_category", () => { + const parser = new ShapeParser(); + const generator = new TypeGenerator({ schemaRegistry: registry }); + const model = generator.generateModelDescriptor("BudgetAccount", parser.parse("federal_account_symbol,account_category,source_anomalies")); + expect(model.fields.map((f) => f.field.name).sort()).toEqual(["account_category", "federal_account_symbol", "source_anomalies"]); + }); + it("relationships(type, source) generates a model descriptor without raising", () => { const parser = new ShapeParser(); const generator = new TypeGenerator({ schemaRegistry: registry }); From cd8d290fdd81faff81f4ca7a12c4bdc8d608f233 Mon Sep 17 00:00:00 2001 From: "V. David Zvenyach" Date: Fri, 2 Oct 2026 16:10:24 -0500 Subject: [PATCH 2/2] feat(budget): data_through_period on budget accounts Supports the Tango API 5.8.0 budget-account field `data_through_period`: the File A fiscal period (1-12) an account-year's figures run through, null with no File A data. `BudgetAccount` gains the field, and `ListBudgetAccountsOptions` types the `data_through_period`, `data_through_period__gte`, `data_through_period__lte` and `data_through_period__isnull` filters. Re-vendors the contract at 5.8.0 and regenerates the shape overlay. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 ++- contracts/filter_shape_contract.json | 27 ++++++++++++++++++++++++++- docs/API_REFERENCE.md | 4 ++++ src/client.ts | 6 ++++++ src/models/BudgetAccount.ts | 2 ++ src/shapes/generatedOverlay.ts | 1 + tests/unit/client.parity.test.ts | 24 ++++++++++++++++++++++++ tests/unit/shapes.overlay.test.ts | 14 +++++++++++--- 8 files changed, 76 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11459dd..1b2e6b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project follows [Semantic Versioning](https://semver.org/). ### Added - **Budget account `source_anomalies` and `account_category`** (Tango API 5.7.0). `BudgetAccount` gains `account_category` (`"budgetary"` or `"credit_financing"` today, typed as an open string) and `source_anomalies`, a list that is `[]` when the row is clean. Each entry is a new `BudgetSourceAnomaly` (with `BudgetSourceAnomalySource` and `BudgetSourceAnomalyRow` for its upstream rows), and every property on it is optional. `action` is `"capped"` when a served value was clamped to its bound and `"flagged"` when it is served as reported; `code` is an open string. `ListBudgetAccountsOptions` types the new `account_category` and `account_category__in` filters, plus the `agency` filter the API already accepted. There is no anomaly filter. The default ordering now puts accounts with a null `enacted_ba` last, which is documented on the options interface and in `docs/API_REFERENCE.md`. +- **Budget account `data_through_period`** (Tango API 5.8.0). `BudgetAccount` gains `data_through_period`, 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; it is null when the row has no File A data. `ListBudgetAccountsOptions` types the `data_through_period`, `data_through_period__gte`, `data_through_period__lte` and `data_through_period__isnull` filters. - **Boards-of-contract-appeals decisions** (Tango API 4.26.0). `listContractAppeals(options)` and `getContractAppeal(uuid, options)` over `/api/contract_appeals/`, with every filter the API accepts declared as a typed option on `ListContractAppealsOptions` (`search`, `board`, `docket`, `appellant`, `judge`, `decision_type`, the `decision_date_after` / `_before` pair, `listed`, `document_id`, `ordering`), the new `ContractAppealRecord` return type, and a registered `ContractAppeal` shape schema so the typed shape API resolves the resource's fields. @@ -30,7 +31,7 @@ This project follows [Semantic Versioning](https://semver.org/). ### Changed -- Re-vendored the contract for Tango API 5.7.0 and regenerated the overlay, so budget accounts, opportunities, notices, exclusions and SBIR shapes resolve the fields the API now serves. Federal Register documents are in the contract without an SDK method yet, so the resource is baselined as a tracked gap in both conformance baselines. +- Re-vendored the contract for Tango API 5.8.0 and regenerated the overlay, so budget accounts, opportunities, notices, exclusions and SBIR shapes resolve the fields the API now serves. Federal Register documents are in the contract without an SDK method yet, so the resource is baselined as a tracked gap in both conformance baselines. - Re-vendored `contracts/filter_shape_contract.json` (schema_version 2, 48 resources) and regenerated `src/shapes/generatedOverlay.ts` from it — 359 fields across 25 containers, 73 nested schemas. - Re-vendored the contract for Tango API 5.1.0 and regenerated the overlay, which now merges a model's expand when two resources embed it instead of letting the narrower copy win. eBuy requests are in the contract without an SDK method yet, so it is baselined as a tracked gap. - Baselined 14 reverse-shape-coverage gaps in `contracts/shape_coverage_baseline.json`, matching tango-python. All 14 are the nested sub-resource routes above, which reuse the parent resource's model rather than carrying one of their own; none is SLED, and none is a regression — they became visible only with the re-vendored contract. diff --git a/contracts/filter_shape_contract.json b/contracts/filter_shape_contract.json index b8b8cac..2d76d8f 100644 --- a/contracts/filter_shape_contract.json +++ b/contracts/filter_shape_contract.json @@ -1,6 +1,6 @@ { "meta": { - "api_version": "5.7.0", + "api_version": "5.8.0", "description": "Canonical API filter/shape contract. Downstream consumers (SDK, MCP) should validate their conformance against this manifest.", "generated_from": "scripts/filter_shape_conformance.py", "schema_version": 2 @@ -1672,6 +1672,10 @@ "contract_share_of_obligated_capped", "contract_share_of_obligated_capped__gte", "contract_share_of_obligated_capped__lte", + "data_through_period", + "data_through_period__gte", + "data_through_period__isnull", + "data_through_period__lte", "enacted_ba", "enacted_ba_5yr_cagr", "enacted_ba_5yr_cagr__gte", @@ -1933,6 +1937,25 @@ "lookup": "lte", "type": "number" }, + "data_through_period": { + "filter_class": "NumberFilter", + "type": "number" + }, + "data_through_period__gte": { + "filter_class": "NumberFilter", + "lookup": "gte", + "type": "number" + }, + "data_through_period__isnull": { + "filter_class": "BooleanFilter", + "lookup": "isnull", + "type": "boolean" + }, + "data_through_period__lte": { + "filter_class": "NumberFilter", + "lookup": "lte", + "type": "number" + }, "enacted_ba": { "filter_class": "NumberFilter", "type": "number" @@ -2304,6 +2327,7 @@ "contract_share_of_obligated", "contract_share_of_obligated_capped", "created", + "data_through_period", "enacted_ba", "enacted_ba_5yr_cagr", "enacted_ba_yoy_pct", @@ -2392,6 +2416,7 @@ "contract_share_of_obligated", "contract_share_of_obligated_capped", "created", + "data_through_period", "enacted_ba", "enacted_ba_5yr_cagr", "enacted_ba_yoy_pct", diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index ad0e8e9..2a8efc6 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -499,6 +499,10 @@ See `ListBudgetAccountsOptions` in `src/client.ts` for the complete list — eve Any of the numeric fields is a valid `ordering` target (`ordering: "-unobligated_balance"` ranks by largest headroom first), and `search` covers account title, agency name, and bureau name. With no `ordering`, rows come back newest fiscal year first, then by `enacted_ba` descending, with accounts that have no `enacted_ba` last. +**Data through period.** Each row carries `data_through_period`, the File A fiscal period (1–12) the account-year's figures run through, so the latest fiscal year is partial until it reaches 12. +It is null when the row has no File A data. +`data_through_period`, `data_through_period__gte`, `data_through_period__lte` and `data_through_period__isnull` filter on it; for example, `data_through_period__isnull: false` keeps only rows with File A data. + **Account category.** Each row carries `account_category` (`"budgetary"` or `"credit_financing"` today; treat it as an open string), and `account_category` / `account_category__in` filter on it. For example, `account_category: "budgetary"` leaves out credit financing accounts. diff --git a/src/client.ts b/src/client.ts index 40c416b..79f8a3e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -428,6 +428,12 @@ export interface ListBudgetAccountsOptions extends ListOptionsBase { fiscal_year_gte?: number | string; /** Legacy alias remapped to `fiscal_year__lte`. */ fiscal_year_lte?: number | string; + /** File A fiscal period (1-12) the account-year's figures run through; below 12 the fiscal year is partial. */ + data_through_period?: number | string; + data_through_period__gte?: number | string; + data_through_period__lte?: number | string; + /** `true` for account-years with no File A data (`data_through_period` is null), `false` for those with it. */ + data_through_period__isnull?: boolean; agency_code?: string; agency_code__in?: string; bureau_name?: string; diff --git a/src/models/BudgetAccount.ts b/src/models/BudgetAccount.ts index 48348cf..ef79710 100644 --- a/src/models/BudgetAccount.ts +++ b/src/models/BudgetAccount.ts @@ -53,6 +53,8 @@ export interface BudgetAccount { id?: number; federal_account_symbol?: string; fiscal_year?: number; + /** File A fiscal period (1-12) this account-year's figures run through; the latest fiscal year is partial until it reaches 12. Null when the row has no File A data. */ + data_through_period?: number | null; agency_code?: string | null; agency_name?: string | null; bureau_name?: string | null; diff --git a/src/shapes/generatedOverlay.ts b/src/shapes/generatedOverlay.ts index 3ebc377..4343ace 100644 --- a/src/shapes/generatedOverlay.ts +++ b/src/shapes/generatedOverlay.ts @@ -700,6 +700,7 @@ export const GENERATED_OVERLAY: Record = { contract_share_of_obligated: f("contract_share_of_obligated", "str"), contract_share_of_obligated_capped: f("contract_share_of_obligated_capped", "str"), created: f("created", "datetime"), + data_through_period: f("data_through_period", "str"), enacted_ba: f("enacted_ba", "str"), enacted_ba_5yr_cagr: f("enacted_ba_5yr_cagr", "str"), enacted_ba_yoy_pct: f("enacted_ba_yoy_pct", "str"), diff --git a/tests/unit/client.parity.test.ts b/tests/unit/client.parity.test.ts index 5741c31..2fccb04 100644 --- a/tests/unit/client.parity.test.ts +++ b/tests/unit/client.parity.test.ts @@ -345,6 +345,30 @@ describe("TangoClient — filter-surface catch-up", () => { expect(params.get("agency")).toBe("DOD"); }); + it("listBudgetAccounts sends the data_through_period filters and returns the field as served", async () => { + const { client, calls } = makeClient({ + count: 2, + next: null, + previous: null, + results: [ + { federal_account_symbol: "097-0100", fiscal_year: 2026, data_through_period: 9 }, + { federal_account_symbol: "097-4000", fiscal_year: 2026, data_through_period: null }, + ], + }); + const resp = await client.listBudgetAccounts({ + data_through_period: 9, + data_through_period__gte: 3, + data_through_period__lte: 11, + data_through_period__isnull: false, + }); + const params = new URL(calls[0].url).searchParams; + expect(params.get("data_through_period")).toBe("9"); + expect(params.get("data_through_period__gte")).toBe("3"); + expect(params.get("data_through_period__lte")).toBe("11"); + expect(params.get("data_through_period__isnull")).toBe("false"); + expect((resp.results as BudgetAccount[]).map((r) => r.data_through_period)).toEqual([9, null]); + }); + it("listBudgetAccounts returns source_anomalies as served", async () => { const anomaly = { code: "contract_exceeds_obligations", diff --git a/tests/unit/shapes.overlay.test.ts b/tests/unit/shapes.overlay.test.ts index 7832e3a..2c7d32b 100644 --- a/tests/unit/shapes.overlay.test.ts +++ b/tests/unit/shapes.overlay.test.ts @@ -59,11 +59,19 @@ describe("Entity relationships shape coverage", () => { } }); - it("BudgetAccount resolves source_anomalies and account_category", () => { + it("BudgetAccount resolves source_anomalies, account_category and data_through_period", () => { const parser = new ShapeParser(); const generator = new TypeGenerator({ schemaRegistry: registry }); - const model = generator.generateModelDescriptor("BudgetAccount", parser.parse("federal_account_symbol,account_category,source_anomalies")); - expect(model.fields.map((f) => f.field.name).sort()).toEqual(["account_category", "federal_account_symbol", "source_anomalies"]); + const model = generator.generateModelDescriptor( + "BudgetAccount", + parser.parse("federal_account_symbol,data_through_period,account_category,source_anomalies"), + ); + expect(model.fields.map((f) => f.field.name).sort()).toEqual([ + "account_category", + "data_through_period", + "federal_account_symbol", + "source_anomalies", + ]); }); it("relationships(type, source) generates a model descriptor without raising", () => {