Skip to content

Commit 772dbb6

Browse files
mivertowskiclaude
andcommitted
feat: align SDK with stabilized API — add 14 endpoints, fix field mappings, expand Company type
Reconcile the SDK against the production VynCo API (v1.6.0): - Company: add 21 missing fields (address, geo, purpose, auditor_name, FINMA, EHRAID, etc.) - CompanyListParams: add 7 filter params (status, legalForm, capitalMin/Max, auditorCategory, sort) - Dashboard types: replace AuditorTenureStats, DataCompleteness, PipelineStatus with API-matching shapes - Fix i32→i64: CreditBalance.used_this_month, BillingSummary.used_this_month, MemberUsage.credits_used - AiSearchResponse.results: Vec<Company> → Vec<serde_json::Value> New endpoints (14): companies: get_full, structure, acquisitions, notes CRUD (4), tags CRUD (4), export_excel dossiers: generate teams: join New types: CompanyFullResponse, PersonEntry, ChangeEntry, RelationshipEntry, CorporateStructure, RelatedCompanyEntry, Note, CreateNoteRequest, UpdateNoteRequest, Tag, CreateTagRequest, TagSummary, ExcelExportRequest, ExcelExportFilter, Acquisition, LongestTenure, JoinTeamRequest, JoinTeamResponse Infrastructure: Client::request_bytes_with_body for POST→raw-bytes (CSV export). Tests: 8 new (45 total), all passing. fmt/clippy clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1758593 commit 772dbb6

9 files changed

Lines changed: 885 additions & 37 deletions

File tree

CLAUDE.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## What This Is
66

7-
Rust SDK (`vynco` crate) for the VynCo Swiss Corporate Intelligence API. Covers 69 public API endpoints across 18 resource modules. Aligned with the VynCo OpenAPI 2.0.0 specification.
7+
Rust SDK (`vynco` crate) for the VynCo Swiss Corporate Intelligence API. Covers 83 public API endpoints across 18 resource modules. Aligned with the VynCo API v1.6.0.
88

99
## Commands
1010

@@ -39,12 +39,12 @@ cargo test -- --nocapture # Run tests with stdout visible
3939

4040
**Error mapping:** HTTP status → `VyncoError` variant: 401→Authentication, 402→InsufficientCredits, 403→Forbidden, 404→NotFound, 400/422→Validation, 409→Conflict, 429→RateLimit, 5xx→Server. Error bodies follow RFC 7807 ProblemDetails with `error_type`, `title`, `status`, `detail` (`Option<String>`), and `instance` (`Option<String>`) fields.
4141

42-
### Resources (18 modules, 69 endpoints)
42+
### Resources (18 modules, 83 endpoints)
4343

4444
| Resource | Endpoints |
4545
|----------|-----------|
4646
| `health` | `check` |
47-
| `companies` | `list`, `get`, `count`, `events`, `statistics`, `compare`, `news`, `reports`, `relationships`, `hierarchy`, `fingerprint`, `nearby` |
47+
| `companies` | `list`, `get`, `get_full`, `count`, `events`, `statistics`, `compare`, `structure`, `acquisitions`, `news`, `reports`, `relationships`, `hierarchy`, `fingerprint`, `nearby`, `notes`, `create_note`, `update_note`, `delete_note`, `tags`, `create_tag`, `delete_tag`, `all_tags`, `export_excel` |
4848
| `auditors` | `history`, `tenures` |
4949
| `dashboard` | `get` |
5050
| `screening` | `screen` |
@@ -55,11 +55,11 @@ cargo test -- --nocapture # Run tests with stdout visible
5555
| `api_keys` | `list`, `create`, `revoke` |
5656
| `credits` | `balance`, `usage`, `history` |
5757
| `billing` | `create_checkout`, `create_portal` |
58-
| `teams` | `me`, `create`, `members`, `invite_member`, `update_member_role`, `remove_member`, `billing_summary` |
58+
| `teams` | `me`, `create`, `members`, `invite_member`, `update_member_role`, `remove_member`, `billing_summary`, `join` |
5959
| `changes` | `list`, `by_company`, `statistics` |
6060
| `persons` | `board_members` |
6161
| `analytics` | `statistics`, `cantons`, `auditors`, `cluster`, `anomalies`, `rfm_segments`, `cohorts`, `candidates` |
62-
| `dossiers` | `create`, `list`, `get`, `delete` |
62+
| `dossiers` | `create`, `list`, `get`, `delete`, `generate` |
6363
| `graph` | `get`, `export`, `analyze` |
6464

6565
### Serde Conventions

examples/vynco_cli.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -338,19 +338,27 @@ async fn run(client: Client, command: Command) -> Result<(), VyncoError> {
338338
Command::Dashboard => {
339339
let resp = client.dashboard().get().await?;
340340
let d = &resp.data.data;
341-
println!("Total companies: {}", d.total_companies);
342-
println!("With canton: {}", d.with_canton);
343-
println!("With status: {}", d.with_status);
344-
println!("With legal form: {}", d.with_legal_form);
345-
println!("With capital: {}", d.with_capital);
346-
println!("Completeness: {:.1}%", d.completeness_pct);
341+
println!("Total companies: {}", d.total_companies);
342+
println!("Enriched: {}", d.enriched_companies);
343+
println!("With industry: {}", d.companies_with_industry);
344+
println!("With geo: {}", d.companies_with_geo);
345+
println!("Total persons: {}", d.total_persons);
346+
println!("Total changes: {}", d.total_changes);
347+
println!("SOGC publications: {}", d.total_sogc_publications);
347348

348349
let t = &resp.data.auditor_tenures;
349350
println!("\nAuditor tenures:");
350-
println!(" Total: {}", t.total_tenures);
351-
println!(" Long (7+yr): {}", t.long_tenures_7plus);
351+
println!(" Tracked: {}", t.total_tracked);
352+
println!(" Current: {}", t.current_auditors);
353+
println!(" Over 7yr: {}", t.tenures_over_7_years);
354+
println!(" Over 10yr: {}", t.tenures_over_10_years);
352355
println!(" Avg years: {:.1}", t.avg_tenure_years);
353-
println!(" Max years: {:.1}", t.max_tenure_years);
356+
if let Some(lt) = &t.longest_tenure {
357+
println!(
358+
" Longest: {:.1}yr — {} ({})",
359+
lt.tenure_years, lt.auditor_name, lt.company_name
360+
);
361+
}
354362
print_meta(&resp.meta);
355363
}
356364

src/blocking.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ impl Companies<'_> {
191191
self.client.block_on(self.client.inner.companies().get(uid))
192192
}
193193

194+
pub fn get_full(&self, uid: &str) -> Result<Response<CompanyFullResponse>> {
195+
self.client
196+
.block_on(self.client.inner.companies().get_full(uid))
197+
}
198+
194199
pub fn count(&self) -> Result<Response<CompanyCount>> {
195200
self.client.block_on(self.client.inner.companies().count())
196201
}
@@ -235,10 +240,70 @@ impl Companies<'_> {
235240
.block_on(self.client.inner.companies().fingerprint(uid))
236241
}
237242

243+
pub fn structure(&self, uid: &str) -> Result<Response<CorporateStructure>> {
244+
self.client
245+
.block_on(self.client.inner.companies().structure(uid))
246+
}
247+
248+
pub fn acquisitions(&self, uid: &str) -> Result<Response<Vec<Acquisition>>> {
249+
self.client
250+
.block_on(self.client.inner.companies().acquisitions(uid))
251+
}
252+
238253
pub fn nearby(&self, params: &NearbyParams) -> Result<Response<Vec<NearbyCompany>>> {
239254
self.client
240255
.block_on(self.client.inner.companies().nearby(params))
241256
}
257+
258+
pub fn notes(&self, uid: &str) -> Result<Response<Vec<Note>>> {
259+
self.client
260+
.block_on(self.client.inner.companies().notes(uid))
261+
}
262+
263+
pub fn create_note(&self, uid: &str, req: &CreateNoteRequest) -> Result<Response<Note>> {
264+
self.client
265+
.block_on(self.client.inner.companies().create_note(uid, req))
266+
}
267+
268+
pub fn update_note(
269+
&self,
270+
uid: &str,
271+
note_id: &str,
272+
req: &UpdateNoteRequest,
273+
) -> Result<Response<Note>> {
274+
self.client
275+
.block_on(self.client.inner.companies().update_note(uid, note_id, req))
276+
}
277+
278+
pub fn delete_note(&self, uid: &str, note_id: &str) -> Result<ResponseMeta> {
279+
self.client
280+
.block_on(self.client.inner.companies().delete_note(uid, note_id))
281+
}
282+
283+
pub fn tags(&self, uid: &str) -> Result<Response<Vec<Tag>>> {
284+
self.client
285+
.block_on(self.client.inner.companies().tags(uid))
286+
}
287+
288+
pub fn create_tag(&self, uid: &str, req: &CreateTagRequest) -> Result<Response<Tag>> {
289+
self.client
290+
.block_on(self.client.inner.companies().create_tag(uid, req))
291+
}
292+
293+
pub fn delete_tag(&self, uid: &str, tag_id: &str) -> Result<ResponseMeta> {
294+
self.client
295+
.block_on(self.client.inner.companies().delete_tag(uid, tag_id))
296+
}
297+
298+
pub fn all_tags(&self) -> Result<Response<Vec<TagSummary>>> {
299+
self.client
300+
.block_on(self.client.inner.companies().all_tags())
301+
}
302+
303+
pub fn export_excel(&self, req: &ExcelExportRequest) -> Result<ExportFile> {
304+
self.client
305+
.block_on(self.client.inner.companies().export_excel(req))
306+
}
242307
}
243308

244309
pub struct Auditors<'a> {
@@ -509,6 +574,10 @@ impl Teams<'_> {
509574
self.client
510575
.block_on(self.client.inner.teams().billing_summary())
511576
}
577+
578+
pub fn join(&self, req: &JoinTeamRequest) -> Result<Response<JoinTeamResponse>> {
579+
self.client.block_on(self.client.inner.teams().join(req))
580+
}
512581
}
513582

514583
pub struct Changes<'a> {
@@ -613,6 +682,11 @@ impl Dossiers<'_> {
613682
self.client
614683
.block_on(self.client.inner.dossiers().delete(id))
615684
}
685+
686+
pub fn generate(&self, uid: &str) -> Result<Response<Dossier>> {
687+
self.client
688+
.block_on(self.client.inner.dossiers().generate(uid))
689+
}
616690
}
617691

618692
pub struct Graph<'a> {

src/client.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,45 @@ impl Client {
262262
Ok((bytes, meta, content_type, filename))
263263
}
264264

265+
/// Send a request with a JSON body and return raw bytes (e.g. CSV exports).
266+
/// Returns `(bytes, meta, content_type, filename)`.
267+
pub(crate) async fn request_bytes_with_body<B: Serialize>(
268+
&self,
269+
method: Method,
270+
path: &str,
271+
body: &B,
272+
) -> Result<(Vec<u8>, ResponseMeta, String, String)> {
273+
let builder = self.http.request(method.clone(), self.url(path)).json(body);
274+
let resp = self.execute_raw(builder).await?;
275+
let meta = ResponseMeta::from_headers(resp.headers());
276+
let status = resp.status();
277+
278+
let content_type = resp
279+
.headers()
280+
.get(header::CONTENT_TYPE)
281+
.and_then(|v| v.to_str().ok())
282+
.unwrap_or("application/octet-stream")
283+
.to_string();
284+
285+
let filename = resp
286+
.headers()
287+
.get(header::CONTENT_DISPOSITION)
288+
.and_then(|v| v.to_str().ok())
289+
.and_then(|v| {
290+
v.split("filename=")
291+
.nth(1)
292+
.map(|f| f.trim_matches('"').to_string())
293+
})
294+
.unwrap_or_default();
295+
296+
if !status.is_success() {
297+
return Err(self.map_error(status, resp).await);
298+
}
299+
300+
let bytes = resp.bytes().await.map_err(VyncoError::Http)?.to_vec();
301+
Ok((bytes, meta, content_type, filename))
302+
}
303+
265304
/// Execute a request with retry logic, returning the deserialized body + metadata.
266305
async fn execute<T: DeserializeOwned>(
267306
&self,

0 commit comments

Comments
 (0)