1use rust_decimal::Decimal;
7use rust_decimal::prelude::Signed;
8
9use super::{BookingError, BookingMethod, BookingResult, Inventory};
10use crate::{Amount, CostSpec, Position};
11
12impl Inventory {
13 pub fn try_reduce(
30 &self,
31 units: &Amount,
32 cost_spec: Option<&CostSpec>,
33 method: BookingMethod,
34 ) -> Result<BookingResult, BookingError> {
35 let spec = cost_spec.cloned().unwrap_or_default();
36
37 match method {
38 BookingMethod::Strict | BookingMethod::StrictWithSize => {
39 self.try_reduce_strict(units, &spec, method == BookingMethod::StrictWithSize)
40 }
41 BookingMethod::Fifo => self.try_reduce_ordered(units, &spec, false),
42 BookingMethod::Lifo => self.try_reduce_ordered(units, &spec, true),
43 BookingMethod::Hifo => self.try_reduce_hifo(units, &spec),
44 BookingMethod::Average => self.try_reduce_average(units),
45 BookingMethod::None => self.try_reduce_ordered(units, &CostSpec::default(), false),
46 }
47 }
48
49 fn try_reduce_strict(
51 &self,
52 units: &Amount,
53 spec: &CostSpec,
54 with_size: bool,
55 ) -> Result<BookingResult, BookingError> {
56 let matching_indices: Vec<usize> = self
57 .positions
58 .iter()
59 .enumerate()
60 .filter(|(_, p)| {
61 p.units.currency == units.currency
62 && !p.is_empty()
63 && p.can_reduce(units)
64 && p.matches_cost_spec(spec)
65 })
66 .map(|(i, _)| i)
67 .collect();
68
69 match matching_indices.len() {
70 0 => Err(BookingError::NoMatchingLot {
71 currency: units.currency.clone(),
72 cost_spec: spec.clone(),
73 }),
74 1 => {
75 let idx = matching_indices[0];
76 self.try_reduce_from_lot(idx, units)
77 }
78 n => {
79 if with_size {
80 let exact_matches: Vec<usize> = matching_indices
82 .iter()
83 .filter(|&&i| self.positions[i].units.number.abs() == units.number.abs())
84 .copied()
85 .collect();
86
87 if exact_matches.is_empty() {
88 let total_units: Decimal = matching_indices
90 .iter()
91 .map(|&i| self.positions[i].units.number.abs())
92 .sum();
93 if total_units == units.number.abs() {
94 self.try_reduce_ordered(units, spec, false)
95 } else {
96 Err(BookingError::AmbiguousMatch {
97 num_matches: n,
98 currency: units.currency.clone(),
99 })
100 }
101 } else {
102 let idx = exact_matches[0];
103 self.try_reduce_from_lot(idx, units)
104 }
105 } else {
106 self.try_reduce_ordered(units, spec, false)
108 }
109 }
110 }
111 }
112
113 fn try_reduce_ordered(
115 &self,
116 units: &Amount,
117 spec: &CostSpec,
118 reverse: bool,
119 ) -> Result<BookingResult, BookingError> {
120 let mut remaining = units.number.abs();
121 let mut matched = Vec::new();
122 let mut cost_basis = Decimal::ZERO;
123 let mut cost_currency = None;
124
125 let mut indices: Vec<usize> = self
127 .positions
128 .iter()
129 .enumerate()
130 .filter(|(_, p)| {
131 p.units.currency == units.currency
132 && !p.is_empty()
133 && p.units.number.signum() != units.number.signum()
134 && p.matches_cost_spec(spec)
135 })
136 .map(|(i, _)| i)
137 .collect();
138
139 indices.sort_by_key(|&i| self.positions[i].cost.as_ref().and_then(|c| c.date));
141
142 if reverse {
143 indices.reverse();
144 }
145
146 if indices.is_empty() {
147 return Err(BookingError::NoMatchingLot {
148 currency: units.currency.clone(),
149 cost_spec: spec.clone(),
150 });
151 }
152
153 for idx in indices {
154 if remaining.is_zero() {
155 break;
156 }
157
158 let pos = &self.positions[idx];
159 let available = pos.units.number.abs();
160 let take = remaining.min(available);
161
162 if let Some(cost) = &pos.cost {
164 cost_basis += take * cost.number;
165 cost_currency = Some(cost.currency.clone());
166 }
167
168 let (taken, _) = pos.split(take * pos.units.number.signum());
170 matched.push(taken);
171
172 remaining -= take;
173 }
174
175 if !remaining.is_zero() {
176 let available = units.number.abs() - remaining;
177 return Err(BookingError::InsufficientUnits {
178 currency: units.currency.clone(),
179 requested: units.number.abs(),
180 available,
181 });
182 }
183
184 Ok(BookingResult {
185 matched,
186 cost_basis: cost_currency.map(|c| Amount::new(cost_basis, c)),
187 })
188 }
189
190 fn try_reduce_hifo(
192 &self,
193 units: &Amount,
194 spec: &CostSpec,
195 ) -> Result<BookingResult, BookingError> {
196 let mut remaining = units.number.abs();
197 let mut matched = Vec::new();
198 let mut cost_basis = Decimal::ZERO;
199 let mut cost_currency = None;
200
201 let mut matching: Vec<(usize, Decimal)> = self
203 .positions
204 .iter()
205 .enumerate()
206 .filter(|(_, p)| {
207 p.units.currency == units.currency
208 && !p.is_empty()
209 && p.units.number.signum() != units.number.signum()
210 && p.matches_cost_spec(spec)
211 })
212 .map(|(i, p)| {
213 let cost = p.cost.as_ref().map_or(Decimal::ZERO, |c| c.number);
214 (i, cost)
215 })
216 .collect();
217
218 if matching.is_empty() {
219 return Err(BookingError::NoMatchingLot {
220 currency: units.currency.clone(),
221 cost_spec: spec.clone(),
222 });
223 }
224
225 matching.sort_by(|a, b| b.1.cmp(&a.1));
227
228 let indices: Vec<usize> = matching.into_iter().map(|(i, _)| i).collect();
229
230 for idx in indices {
231 if remaining.is_zero() {
232 break;
233 }
234
235 let pos = &self.positions[idx];
236 let available = pos.units.number.abs();
237 let take = remaining.min(available);
238
239 if let Some(cost) = &pos.cost {
241 cost_basis += take * cost.number;
242 cost_currency = Some(cost.currency.clone());
243 }
244
245 let (taken, _) = pos.split(take * pos.units.number.signum());
247 matched.push(taken);
248
249 remaining -= take;
250 }
251
252 if !remaining.is_zero() {
253 let available = units.number.abs() - remaining;
254 return Err(BookingError::InsufficientUnits {
255 currency: units.currency.clone(),
256 requested: units.number.abs(),
257 available,
258 });
259 }
260
261 Ok(BookingResult {
262 matched,
263 cost_basis: cost_currency.map(|c| Amount::new(cost_basis, c)),
264 })
265 }
266
267 fn try_reduce_average(&self, units: &Amount) -> Result<BookingResult, BookingError> {
269 let total_units: Decimal = self
270 .positions
271 .iter()
272 .filter(|p| p.units.currency == units.currency && !p.is_empty())
273 .map(|p| p.units.number)
274 .sum();
275
276 if total_units.is_zero() {
277 return Err(BookingError::InsufficientUnits {
278 currency: units.currency.clone(),
279 requested: units.number.abs(),
280 available: Decimal::ZERO,
281 });
282 }
283
284 let reduction = units.number.abs();
285 if reduction > total_units.abs() {
286 return Err(BookingError::InsufficientUnits {
287 currency: units.currency.clone(),
288 requested: reduction,
289 available: total_units.abs(),
290 });
291 }
292
293 let book_values = self.book_value(&units.currency);
294 let cost_basis = if let Some((curr, &total)) = book_values.iter().next() {
295 let per_unit_cost = total / total_units;
296 Some(Amount::new(reduction * per_unit_cost, curr.clone()))
297 } else {
298 None
299 };
300
301 let matched: Vec<Position> = self
302 .positions
303 .iter()
304 .filter(|p| p.units.currency == units.currency && !p.is_empty())
305 .cloned()
306 .collect();
307
308 Ok(BookingResult {
309 matched,
310 cost_basis,
311 })
312 }
313
314 fn try_reduce_from_lot(
316 &self,
317 idx: usize,
318 units: &Amount,
319 ) -> Result<BookingResult, BookingError> {
320 let pos = &self.positions[idx];
321 let available = pos.units.number.abs();
322 let requested = units.number.abs();
323
324 if requested > available {
325 return Err(BookingError::InsufficientUnits {
326 currency: units.currency.clone(),
327 requested,
328 available,
329 });
330 }
331
332 let cost_basis = pos.cost.as_ref().map(|c| c.total_cost(requested));
333 let (matched, _) = pos.split(requested * pos.units.number.signum());
334
335 Ok(BookingResult {
336 matched: vec![matched],
337 cost_basis,
338 })
339 }
340}
341
342impl Inventory {
343 pub(super) fn reduce_strict(
346 &mut self,
347 units: &Amount,
348 spec: &CostSpec,
349 ) -> Result<BookingResult, BookingError> {
350 let matching_indices: Vec<usize> = self
351 .positions
352 .iter()
353 .enumerate()
354 .filter(|(_, p)| {
355 p.units.currency == units.currency
356 && !p.is_empty()
357 && p.can_reduce(units)
358 && p.matches_cost_spec(spec)
359 })
360 .map(|(i, _)| i)
361 .collect();
362
363 match matching_indices.len() {
364 0 => Err(BookingError::NoMatchingLot {
365 currency: units.currency.clone(),
366 cost_spec: spec.clone(),
367 }),
368 1 => {
369 let idx = matching_indices[0];
370 self.reduce_from_lot(idx, units)
371 }
372 _n => {
373 self.reduce_ordered(units, spec, false)
378 }
379 }
380 }
381
382 pub(super) fn reduce_strict_with_size(
384 &mut self,
385 units: &Amount,
386 spec: &CostSpec,
387 ) -> Result<BookingResult, BookingError> {
388 let matching_indices: Vec<usize> = self
389 .positions
390 .iter()
391 .enumerate()
392 .filter(|(_, p)| {
393 p.units.currency == units.currency
394 && !p.is_empty()
395 && p.can_reduce(units)
396 && p.matches_cost_spec(spec)
397 })
398 .map(|(i, _)| i)
399 .collect();
400
401 match matching_indices.len() {
402 0 => Err(BookingError::NoMatchingLot {
403 currency: units.currency.clone(),
404 cost_spec: spec.clone(),
405 }),
406 1 => {
407 let idx = matching_indices[0];
408 self.reduce_from_lot(idx, units)
409 }
410 n => {
411 let exact_matches: Vec<usize> = matching_indices
413 .iter()
414 .filter(|&&i| self.positions[i].units.number.abs() == units.number.abs())
415 .copied()
416 .collect();
417
418 if exact_matches.is_empty() {
419 let total_units: Decimal = matching_indices
421 .iter()
422 .map(|&i| self.positions[i].units.number.abs())
423 .sum();
424 if total_units == units.number.abs() {
425 self.reduce_ordered(units, spec, false)
426 } else {
427 Err(BookingError::AmbiguousMatch {
428 num_matches: n,
429 currency: units.currency.clone(),
430 })
431 }
432 } else {
433 let idx = exact_matches[0];
435 self.reduce_from_lot(idx, units)
436 }
437 }
438 }
439 }
440
441 pub(super) fn reduce_fifo(
443 &mut self,
444 units: &Amount,
445 spec: &CostSpec,
446 ) -> Result<BookingResult, BookingError> {
447 self.reduce_ordered(units, spec, false)
448 }
449
450 pub(super) fn reduce_lifo(
452 &mut self,
453 units: &Amount,
454 spec: &CostSpec,
455 ) -> Result<BookingResult, BookingError> {
456 self.reduce_ordered(units, spec, true)
457 }
458
459 pub(super) fn reduce_hifo(
461 &mut self,
462 units: &Amount,
463 spec: &CostSpec,
464 ) -> Result<BookingResult, BookingError> {
465 let mut remaining = units.number.abs();
466 let mut matched = Vec::new();
467 let mut cost_basis = Decimal::ZERO;
468 let mut cost_currency = None;
469
470 let mut matching: Vec<(usize, Decimal)> = self
472 .positions
473 .iter()
474 .enumerate()
475 .filter(|(_, p)| {
476 p.units.currency == units.currency
477 && !p.is_empty()
478 && p.units.number.signum() != units.number.signum()
479 && p.matches_cost_spec(spec)
480 })
481 .map(|(i, p)| {
482 let cost = p.cost.as_ref().map_or(Decimal::ZERO, |c| c.number);
483 (i, cost)
484 })
485 .collect();
486
487 if matching.is_empty() {
488 return Err(BookingError::NoMatchingLot {
489 currency: units.currency.clone(),
490 cost_spec: spec.clone(),
491 });
492 }
493
494 matching.sort_by(|a, b| b.1.cmp(&a.1));
496
497 let indices: Vec<usize> = matching.into_iter().map(|(i, _)| i).collect();
498
499 for idx in indices {
500 if remaining.is_zero() {
501 break;
502 }
503
504 let pos = &self.positions[idx];
505 let available = pos.units.number.abs();
506 let take = remaining.min(available);
507
508 if let Some(cost) = &pos.cost {
510 cost_basis += take * cost.number;
511 cost_currency = Some(cost.currency.clone());
512 }
513
514 let (taken, _) = pos.split(take * pos.units.number.signum());
516 matched.push(taken);
517
518 let reduction = if units.number.is_sign_negative() {
520 -take
521 } else {
522 take
523 };
524
525 let new_pos = Position {
526 units: Amount::new(pos.units.number + reduction, pos.units.currency.clone()),
527 cost: pos.cost.clone(),
528 };
529 self.positions[idx] = new_pos;
530
531 remaining -= take;
532 }
533
534 if !remaining.is_zero() {
535 let available = units.number.abs() - remaining;
536 return Err(BookingError::InsufficientUnits {
537 currency: units.currency.clone(),
538 requested: units.number.abs(),
539 available,
540 });
541 }
542
543 self.positions.retain(|p| !p.is_empty());
545 self.rebuild_index();
546
547 Ok(BookingResult {
548 matched,
549 cost_basis: cost_currency.map(|c| Amount::new(cost_basis, c)),
550 })
551 }
552
553 pub(super) fn reduce_ordered(
555 &mut self,
556 units: &Amount,
557 spec: &CostSpec,
558 reverse: bool,
559 ) -> Result<BookingResult, BookingError> {
560 let mut remaining = units.number.abs();
561 let mut matched = Vec::new();
562 let mut cost_basis = Decimal::ZERO;
563 let mut cost_currency = None;
564
565 let mut indices: Vec<usize> = self
567 .positions
568 .iter()
569 .enumerate()
570 .filter(|(_, p)| {
571 p.units.currency == units.currency
572 && !p.is_empty()
573 && p.units.number.signum() != units.number.signum()
574 && p.matches_cost_spec(spec)
575 })
576 .map(|(i, _)| i)
577 .collect();
578
579 indices.sort_by_key(|&i| self.positions[i].cost.as_ref().and_then(|c| c.date));
582
583 if reverse {
584 indices.reverse();
585 }
586
587 if indices.is_empty() {
588 return Err(BookingError::NoMatchingLot {
589 currency: units.currency.clone(),
590 cost_spec: spec.clone(),
591 });
592 }
593
594 if let Some(&first_idx) = indices.first() {
596 if let Some(cost) = &self.positions[first_idx].cost {
597 cost_currency = Some(cost.currency.clone());
598 }
599 }
600
601 for idx in indices {
602 if remaining.is_zero() {
603 break;
604 }
605
606 let pos = &mut self.positions[idx];
607 let available = pos.units.number.abs();
608 let take = remaining.min(available);
609
610 if let Some(cost) = &pos.cost {
612 cost_basis += take * cost.number;
613 }
614
615 let (taken, _) = pos.split(take * pos.units.number.signum());
617 matched.push(taken);
618
619 let reduction = if units.number.is_sign_negative() {
621 -take
622 } else {
623 take
624 };
625 pos.units.number += reduction;
626
627 remaining -= take;
628 }
629
630 if !remaining.is_zero() {
631 let available = units.number.abs() - remaining;
632 return Err(BookingError::InsufficientUnits {
633 currency: units.currency.clone(),
634 requested: units.number.abs(),
635 available,
636 });
637 }
638
639 self.positions.retain(|p| !p.is_empty());
641 self.rebuild_index();
642
643 Ok(BookingResult {
644 matched,
645 cost_basis: cost_currency.map(|c| Amount::new(cost_basis, c)),
646 })
647 }
648
649 pub(super) fn reduce_average(&mut self, units: &Amount) -> Result<BookingResult, BookingError> {
651 let total_units: Decimal = self
653 .positions
654 .iter()
655 .filter(|p| p.units.currency == units.currency && !p.is_empty())
656 .map(|p| p.units.number)
657 .sum();
658
659 if total_units.is_zero() {
660 return Err(BookingError::InsufficientUnits {
661 currency: units.currency.clone(),
662 requested: units.number.abs(),
663 available: Decimal::ZERO,
664 });
665 }
666
667 let reduction = units.number.abs();
669 if reduction > total_units.abs() {
670 return Err(BookingError::InsufficientUnits {
671 currency: units.currency.clone(),
672 requested: reduction,
673 available: total_units.abs(),
674 });
675 }
676
677 let book_values = self.book_value(&units.currency);
679 let cost_basis = if let Some((curr, &total)) = book_values.iter().next() {
680 let per_unit_cost = total / total_units;
681 Some(Amount::new(reduction * per_unit_cost, curr.clone()))
682 } else {
683 None
684 };
685
686 let new_units = total_units + units.number;
688
689 let matched: Vec<Position> = self
691 .positions
692 .iter()
693 .filter(|p| p.units.currency == units.currency && !p.is_empty())
694 .cloned()
695 .collect();
696
697 self.positions
698 .retain(|p| p.units.currency != units.currency);
699
700 if !new_units.is_zero() {
702 self.positions.push(Position::simple(Amount::new(
703 new_units,
704 units.currency.clone(),
705 )));
706 }
707
708 self.rebuild_index();
710
711 Ok(BookingResult {
712 matched,
713 cost_basis,
714 })
715 }
716
717 pub(super) fn reduce_none(&mut self, units: &Amount) -> Result<BookingResult, BookingError> {
719 let total_units = self.units(&units.currency);
721
722 if total_units.signum() == units.number.signum() || total_units.is_zero() {
724 self.add(Position::simple(units.clone()));
726 return Ok(BookingResult {
727 matched: vec![],
728 cost_basis: None,
729 });
730 }
731
732 let available = total_units.abs();
733 let requested = units.number.abs();
734
735 if requested > available {
736 return Err(BookingError::InsufficientUnits {
737 currency: units.currency.clone(),
738 requested,
739 available,
740 });
741 }
742
743 self.reduce_ordered(units, &CostSpec::default(), false)
745 }
746
747 pub(super) fn reduce_from_lot(
749 &mut self,
750 idx: usize,
751 units: &Amount,
752 ) -> Result<BookingResult, BookingError> {
753 let pos = &self.positions[idx];
754 let available = pos.units.number.abs();
755 let requested = units.number.abs();
756
757 if requested > available {
758 return Err(BookingError::InsufficientUnits {
759 currency: units.currency.clone(),
760 requested,
761 available,
762 });
763 }
764
765 let cost_basis = pos.cost.as_ref().map(|c| c.total_cost(requested));
767
768 let (matched, _) = pos.split(requested * pos.units.number.signum());
770
771 let currency = pos.units.currency.clone();
773 let new_units = pos.units.number + units.number;
774 let new_pos = Position {
775 units: Amount::new(new_units, currency.clone()),
776 cost: pos.cost.clone(),
777 };
778 self.positions[idx] = new_pos;
779
780 if let Some(cached) = self.units_cache.get_mut(¤cy) {
782 *cached += units.number;
783 }
784
785 if self.positions[idx].is_empty() {
787 self.positions.remove(idx);
788 self.simple_index.clear();
790 for (i, p) in self.positions.iter().enumerate() {
791 if p.cost.is_none() {
792 self.simple_index.insert(p.units.currency.clone(), i);
793 }
794 }
795 }
796
797 Ok(BookingResult {
798 matched: vec![matched],
799 cost_basis,
800 })
801 }
802}