@@ -3,6 +3,17 @@ use reqwest::blocking::Client;
33use serde:: { Deserialize , Serialize } ;
44use std:: time:: Duration ;
55
6+ /// Number of boards to use for availability and pricing calculations
7+ const NUM_BOARDS : i32 = 20 ;
8+
9+ /// Availability tier for offer selection
10+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , PartialOrd , Ord ) ]
11+ enum AvailabilityTier {
12+ NoInventory = 0 ,
13+ Limited = 1 ,
14+ Plenty = 2 ,
15+ }
16+
617/// Price break structure
718#[ derive( Debug , Clone , Deserialize , Serialize ) ]
819pub struct PriceBreak {
@@ -91,6 +102,83 @@ pub struct MatchBomResponse {
91102 pub results : Vec < DesignMatchResult > ,
92103}
93104
105+ /// Check if this is a small generic passive requiring higher stock threshold
106+ fn is_small_generic_passive ( entry : & pcb_sch:: BomEntry ) -> bool {
107+ let is_generic_passive = matches ! (
108+ entry. generic_data,
109+ Some ( pcb_sch:: GenericComponent :: Resistor ( _) | pcb_sch:: GenericComponent :: Capacitor ( _) )
110+ ) ;
111+ let is_small_package = matches ! ( entry. package. as_deref( ) , Some ( "0201" | "0402" | "0603" ) ) ;
112+
113+ is_generic_passive && is_small_package
114+ }
115+
116+ /// Determine the availability tier for an offer
117+ fn get_availability_tier ( stock : i32 , qty : i32 , is_small_passive : bool ) -> AvailabilityTier {
118+ if stock == 0 {
119+ AvailabilityTier :: NoInventory
120+ } else {
121+ let required_stock = if is_small_passive {
122+ 100
123+ } else {
124+ qty * NUM_BOARDS
125+ } ;
126+ if stock >= required_stock {
127+ AvailabilityTier :: Plenty
128+ } else {
129+ AvailabilityTier :: Limited
130+ }
131+ }
132+ }
133+
134+ /// Rank availability tier (lower is better)
135+ #[ inline]
136+ fn tier_rank ( tier : AvailabilityTier ) -> u8 {
137+ match tier {
138+ AvailabilityTier :: Plenty => 0 ,
139+ AvailabilityTier :: Limited => 1 ,
140+ AvailabilityTier :: NoInventory => 2 ,
141+ }
142+ }
143+
144+ /// Compare offers within the same tier: prefer lower price, then higher stock
145+ #[ inline]
146+ fn within_tier_cmp ( a : & OfferWithMatchKey , b : & OfferWithMatchKey , qty : i32 ) -> std:: cmp:: Ordering {
147+ use std:: cmp:: Ordering :: * ;
148+
149+ let price_a = a. unit_price_at_qty ( qty * NUM_BOARDS ) ;
150+ let price_b = b. unit_price_at_qty ( qty * NUM_BOARDS ) ;
151+
152+ match ( price_a, price_b) {
153+ ( Some ( pa) , Some ( pb) ) => pa
154+ . partial_cmp ( & pb)
155+ . unwrap_or ( Equal )
156+ . then_with ( || b. stock_available . cmp ( & a. stock_available ) ) ,
157+ ( None , None ) => b. stock_available . cmp ( & a. stock_available ) ,
158+ ( Some ( _) , None ) => Less ,
159+ ( None , Some ( _) ) => Greater ,
160+ }
161+ }
162+
163+ /// Select the best offer: Plenty > Limited > None tier, then lowest price within tier.
164+ /// Single-pass, allocation-free selection using iterator comparator.
165+ fn select_best_offer (
166+ offers : & [ OfferWithMatchKey ] ,
167+ qty : i32 ,
168+ is_small_passive : bool ,
169+ ) -> Option < & OfferWithMatchKey > {
170+ offers. iter ( ) . min_by ( |a, b| {
171+ let stock_a = a. stock_available . unwrap_or ( 0 ) ;
172+ let stock_b = b. stock_available . unwrap_or ( 0 ) ;
173+ let tier_a = get_availability_tier ( stock_a, qty, is_small_passive) ;
174+ let tier_b = get_availability_tier ( stock_b, qty, is_small_passive) ;
175+
176+ tier_rank ( tier_a)
177+ . cmp ( & tier_rank ( tier_b) )
178+ . then_with ( || within_tier_cmp ( a, b, qty) )
179+ } )
180+ }
181+
94182/// Fetch BOM matching results from the API and populate availability data
95183pub fn fetch_and_populate_availability ( auth_token : & str , bom : & mut pcb_sch:: Bom ) -> Result < ( ) > {
96184 let api_base_url = crate :: get_api_base_url ( ) ;
@@ -136,47 +224,51 @@ pub fn fetch_and_populate_availability(auth_token: &str, bom: &mut pcb_sch::Bom)
136224 . filter ( |( p, _) | p. as_str ( ) == path)
137225 . count ( ) as i32 ;
138226
139- let mut stock_total = 0 ;
140- let mut cheapest_single = None ;
141- let mut cheapest_20x = None ;
142- let mut lcsc_part_id = None ;
143- let mut product_url = None ;
144-
145- for offer in & result. offers {
146- if offer. distributor . as_deref ( ) == Some ( "lcsc" ) && lcsc_part_id. is_none ( ) {
147- lcsc_part_id = offer. distributor_part_id . as_ref ( ) . map ( |id| {
148- // Ensure LCSC part IDs have "C" prefix
149- if id. starts_with ( 'C' ) {
150- id. clone ( )
151- } else {
152- format ! ( "C{}" , id)
153- }
154- } ) ;
155- product_url = offer. product_url . clone ( ) ;
156- }
227+ // Get BOM entry to check if it's a small generic passive
228+ let bom_entry = bom. entries . get ( path) . unwrap ( ) ;
229+ let is_small_passive = is_small_generic_passive ( bom_entry) ;
157230
158- stock_total += offer. stock_available . unwrap_or ( 0 ) ;
231+ // Select the best offer based on availability tier (Plenty > Limited > None)
232+ // then price within tier. This single offer is used for all downstream data.
233+ let best_offer = select_best_offer ( & result. offers , qty, is_small_passive) ;
159234
160- if let Some ( unit_price ) = offer. unit_price_at_qty ( qty ) {
161- cheapest_single =
162- Some ( cheapest_single . map_or ( unit_price , | p : f64 | p . min ( unit_price ) ) ) ;
163- }
235+ // Extract all data from the best matched offer
236+ let ( stock_total , lcsc_part_ids , price_single , price_boards ) = match best_offer {
237+ Some ( offer ) => {
238+ let stock = offer . stock_available . unwrap_or ( 0 ) ;
164239
165- if let Some ( unit_price) = offer. unit_price_at_qty ( qty * 20 ) {
166- let total_price_20x = unit_price * ( qty * 20 ) as f64 ;
167- cheapest_20x =
168- Some ( cheapest_20x. map_or ( total_price_20x, |p : f64 | p. min ( total_price_20x) ) ) ;
240+ let lcsc_id = match (
241+ offer. distributor . as_deref ( ) ,
242+ & offer. distributor_part_id ,
243+ & offer. product_url ,
244+ ) {
245+ ( Some ( "lcsc" ) , Some ( id) , Some ( url) ) => {
246+ let formatted_id = if id. starts_with ( 'C' ) {
247+ id. clone ( )
248+ } else {
249+ format ! ( "C{}" , id)
250+ } ;
251+ vec ! [ ( formatted_id, url. clone( ) ) ]
252+ }
253+ _ => Vec :: new ( ) ,
254+ } ;
255+
256+ let single = offer. unit_price_at_qty ( qty) ;
257+ let unit_boards = offer. unit_price_at_qty ( qty * NUM_BOARDS ) ;
258+ let total_boards = unit_boards. map ( |p| p * ( qty * NUM_BOARDS ) as f64 ) ;
259+
260+ ( stock, lcsc_id, single, total_boards)
169261 }
170- }
262+ None => ( 0 , Vec :: new ( ) , None , None ) ,
263+ } ;
171264
172265 bom. availability . insert (
173266 path. to_string ( ) ,
174267 pcb_sch:: AvailabilityData {
175268 stock_total,
176- price_single : cheapest_single,
177- price_20x : cheapest_20x,
178- lcsc_part_id,
179- product_url,
269+ price_single,
270+ price_boards,
271+ lcsc_part_ids,
180272 } ,
181273 ) ;
182274 }
0 commit comments