Skip to main content

datasynth_generators/industry/retail/
anomalies.rs

1//! Retail-specific anomalies.
2
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6use super::super::common::IndustryAnomaly;
7
8/// Retail-specific anomaly types.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub enum RetailAnomaly {
11    /// Employee gives unauthorized discounts to friends/family.
12    Sweethearting {
13        cashier_id: String,
14        beneficiary: String,
15        estimated_loss: Decimal,
16        transaction_count: u32,
17    },
18    /// Cash stolen from register before recording.
19    Skimming {
20        register_id: String,
21        store_id: String,
22        amount: Decimal,
23        detection_method: String,
24    },
25    /// Fraudulent refunds processed.
26    RefundFraud {
27        transaction_id: String,
28        employee_id: String,
29        refund_amount: Decimal,
30        scheme_type: RefundFraudType,
31    },
32    /// Receiving fraud (short shipments, diversions).
33    ReceivingFraud {
34        po_id: String,
35        employee_id: String,
36        short_quantity: u32,
37        value: Decimal,
38    },
39    /// Fraudulent inter-store transfers.
40    TransferFraud {
41        from_store: String,
42        to_store: String,
43        items_diverted: u32,
44        value: Decimal,
45    },
46    /// Coupon/promotion fraud.
47    CouponFraud {
48        coupon_code: String,
49        not_presented: bool,
50        value: Decimal,
51        transaction_count: u32,
52    },
53    /// Employee discount abuse.
54    EmployeeDiscountAbuse {
55        employee_id: String,
56        non_employee_beneficiary: String,
57        discount_value: Decimal,
58        transaction_count: u32,
59    },
60    /// Void abuse.
61    VoidAbuse {
62        cashier_id: String,
63        void_count: u32,
64        void_total: Decimal,
65        period_days: u32,
66    },
67    /// Price override abuse.
68    PriceOverrideAbuse {
69        employee_id: String,
70        override_count: u32,
71        total_discount: Decimal,
72    },
73    /// Gift card fraud.
74    GiftCardFraud {
75        scheme_type: GiftCardFraudType,
76        amount: Decimal,
77        cards_affected: u32,
78    },
79    /// Inventory manipulation.
80    InventoryManipulation {
81        store_id: String,
82        manipulation_type: InventoryManipulationType,
83        value: Decimal,
84    },
85    /// Fictitious vendor kickback.
86    VendorKickback {
87        vendor_id: String,
88        buyer_id: String,
89        kickback_amount: Decimal,
90        scheme_duration_days: u32,
91    },
92}
93
94/// Type of refund fraud.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
96pub enum RefundFraudType {
97    /// Refund to personal card.
98    RefundToPersonalCard,
99    /// Fake merchandise return.
100    FakeMerchandiseReturn,
101    /// Return without receipt fraud.
102    NoReceiptFraud,
103    /// Cross-retailer return fraud.
104    CrossRetailerFraud,
105    /// Wardrobing (return after use).
106    Wardrobing,
107}
108
109/// Type of gift card fraud.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
111pub enum GiftCardFraudType {
112    /// Loading without payment.
113    LoadingWithoutPayment,
114    /// Balance transfer scheme.
115    BalanceTransfer,
116    /// Card number harvesting.
117    CardNumberHarvesting,
118    /// Return to gift card scheme.
119    ReturnToGiftCard,
120}
121
122/// Type of inventory manipulation.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
124pub enum InventoryManipulationType {
125    /// Phantom inventory.
126    PhantomInventory,
127    /// Concealed shrinkage.
128    ConcealedShrinkage,
129    /// Count manipulation.
130    CountManipulation,
131    /// Category shifting.
132    CategoryShifting,
133}
134
135impl IndustryAnomaly for RetailAnomaly {
136    fn anomaly_type(&self) -> &str {
137        match self {
138            RetailAnomaly::Sweethearting { .. } => "sweethearting",
139            RetailAnomaly::Skimming { .. } => "skimming",
140            RetailAnomaly::RefundFraud { .. } => "refund_fraud",
141            RetailAnomaly::ReceivingFraud { .. } => "receiving_fraud",
142            RetailAnomaly::TransferFraud { .. } => "transfer_fraud",
143            RetailAnomaly::CouponFraud { .. } => "coupon_fraud",
144            RetailAnomaly::EmployeeDiscountAbuse { .. } => "employee_discount_abuse",
145            RetailAnomaly::VoidAbuse { .. } => "void_abuse",
146            RetailAnomaly::PriceOverrideAbuse { .. } => "price_override_abuse",
147            RetailAnomaly::GiftCardFraud { .. } => "gift_card_fraud",
148            RetailAnomaly::InventoryManipulation { .. } => "inventory_manipulation",
149            RetailAnomaly::VendorKickback { .. } => "vendor_kickback",
150        }
151    }
152
153    fn severity(&self) -> u8 {
154        match self {
155            RetailAnomaly::EmployeeDiscountAbuse { .. } => 3,
156            RetailAnomaly::CouponFraud { .. } => 3,
157            RetailAnomaly::VoidAbuse { .. } => 3,
158            RetailAnomaly::PriceOverrideAbuse { .. } => 3,
159            RetailAnomaly::Sweethearting { .. } => 4,
160            RetailAnomaly::RefundFraud { .. } => 4,
161            RetailAnomaly::TransferFraud { .. } => 4,
162            RetailAnomaly::Skimming { .. } => 5,
163            RetailAnomaly::ReceivingFraud { .. } => 5,
164            RetailAnomaly::GiftCardFraud { .. } => 4,
165            RetailAnomaly::InventoryManipulation { .. } => 4,
166            RetailAnomaly::VendorKickback { .. } => 5,
167        }
168    }
169
170    fn detection_difficulty(&self) -> &str {
171        match self {
172            RetailAnomaly::VoidAbuse { .. } => "easy",
173            RetailAnomaly::PriceOverrideAbuse { .. } => "easy",
174            RetailAnomaly::EmployeeDiscountAbuse { .. } => "moderate",
175            RetailAnomaly::CouponFraud { .. } => "moderate",
176            RetailAnomaly::RefundFraud { .. } => "moderate",
177            RetailAnomaly::TransferFraud { .. } => "moderate",
178            RetailAnomaly::Sweethearting { .. } => "hard",
179            RetailAnomaly::GiftCardFraud { .. } => "hard",
180            RetailAnomaly::InventoryManipulation { .. } => "hard",
181            RetailAnomaly::Skimming { .. } => "expert",
182            RetailAnomaly::ReceivingFraud { .. } => "hard",
183            RetailAnomaly::VendorKickback { .. } => "expert",
184        }
185    }
186
187    fn indicators(&self) -> Vec<String> {
188        match self {
189            RetailAnomaly::Sweethearting { .. } => vec![
190                "high_no_sale_rate".to_string(),
191                "frequent_price_overrides".to_string(),
192                "repeat_customer_discounts".to_string(),
193                "lower_avg_transaction".to_string(),
194            ],
195            RetailAnomaly::Skimming { .. } => vec![
196                "register_short".to_string(),
197                "cash_variance_pattern".to_string(),
198                "transaction_gaps".to_string(),
199            ],
200            RetailAnomaly::RefundFraud { .. } => vec![
201                "high_refund_rate".to_string(),
202                "refunds_to_same_card".to_string(),
203                "refunds_without_receipt".to_string(),
204                "customer_not_present_refunds".to_string(),
205            ],
206            RetailAnomaly::VoidAbuse { .. } => vec![
207                "high_void_rate".to_string(),
208                "voids_after_tender".to_string(),
209                "pattern_of_small_voids".to_string(),
210            ],
211            RetailAnomaly::GiftCardFraud { .. } => vec![
212                "gift_cards_activated_without_sale".to_string(),
213                "unusual_gift_card_patterns".to_string(),
214                "gift_card_balance_anomalies".to_string(),
215            ],
216            RetailAnomaly::InventoryManipulation { .. } => vec![
217                "shrinkage_pattern_anomaly".to_string(),
218                "count_timing_manipulation".to_string(),
219                "category_variance_spike".to_string(),
220            ],
221            _ => vec!["general_retail_anomaly".to_string()],
222        }
223    }
224
225    fn regulatory_concerns(&self) -> Vec<String> {
226        match self {
227            RetailAnomaly::Skimming { .. }
228            | RetailAnomaly::ReceivingFraud { .. }
229            | RetailAnomaly::VendorKickback { .. } => vec![
230                "financial_statement_fraud".to_string(),
231                "employee_theft".to_string(),
232                "internal_controls".to_string(),
233            ],
234            RetailAnomaly::InventoryManipulation { .. } => vec![
235                "inventory_valuation".to_string(),
236                "asc_330".to_string(),
237                "sox_section_404".to_string(),
238            ],
239            _ => vec![
240                "employee_theft".to_string(),
241                "internal_controls".to_string(),
242            ],
243        }
244    }
245}
246
247impl RetailAnomaly {
248    /// Returns the financial impact of this anomaly.
249    pub fn financial_impact(&self) -> Decimal {
250        match self {
251            RetailAnomaly::Sweethearting { estimated_loss, .. } => *estimated_loss,
252            RetailAnomaly::Skimming { amount, .. } => *amount,
253            RetailAnomaly::RefundFraud { refund_amount, .. } => *refund_amount,
254            RetailAnomaly::ReceivingFraud { value, .. } => *value,
255            RetailAnomaly::TransferFraud { value, .. } => *value,
256            RetailAnomaly::CouponFraud { value, .. } => *value,
257            RetailAnomaly::EmployeeDiscountAbuse { discount_value, .. } => *discount_value,
258            RetailAnomaly::VoidAbuse { void_total, .. } => *void_total,
259            RetailAnomaly::PriceOverrideAbuse { total_discount, .. } => *total_discount,
260            RetailAnomaly::GiftCardFraud { amount, .. } => *amount,
261            RetailAnomaly::InventoryManipulation { value, .. } => *value,
262            RetailAnomaly::VendorKickback {
263                kickback_amount, ..
264            } => *kickback_amount,
265        }
266    }
267
268    /// Returns whether this involves collusion.
269    pub fn involves_collusion(&self) -> bool {
270        matches!(
271            self,
272            RetailAnomaly::ReceivingFraud { .. }
273                | RetailAnomaly::TransferFraud { .. }
274                | RetailAnomaly::VendorKickback { .. }
275        )
276    }
277}
278
279#[cfg(test)]
280#[allow(clippy::unwrap_used)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_sweethearting() {
286        let anomaly = RetailAnomaly::Sweethearting {
287            cashier_id: "C001".to_string(),
288            beneficiary: "Friend".to_string(),
289            estimated_loss: Decimal::new(500, 0),
290            transaction_count: 20,
291        };
292
293        assert_eq!(anomaly.anomaly_type(), "sweethearting");
294        assert_eq!(anomaly.severity(), 4);
295        assert_eq!(anomaly.detection_difficulty(), "hard");
296        assert_eq!(anomaly.financial_impact(), Decimal::new(500, 0));
297    }
298
299    #[test]
300    fn test_skimming() {
301        let anomaly = RetailAnomaly::Skimming {
302            register_id: "R01".to_string(),
303            store_id: "S001".to_string(),
304            amount: Decimal::new(1000, 0),
305            detection_method: "variance_analysis".to_string(),
306        };
307
308        assert_eq!(anomaly.severity(), 5);
309        assert_eq!(anomaly.detection_difficulty(), "expert");
310    }
311
312    #[test]
313    fn test_collusion() {
314        let kickback = RetailAnomaly::VendorKickback {
315            vendor_id: "V001".to_string(),
316            buyer_id: "B001".to_string(),
317            kickback_amount: Decimal::new(5000, 0),
318            scheme_duration_days: 180,
319        };
320
321        assert!(kickback.involves_collusion());
322
323        let skimming = RetailAnomaly::Skimming {
324            register_id: "R01".to_string(),
325            store_id: "S001".to_string(),
326            amount: Decimal::new(500, 0),
327            detection_method: "".to_string(),
328        };
329
330        assert!(!skimming.involves_collusion());
331    }
332}