1use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6use super::super::common::IndustryAnomaly;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub enum RetailAnomaly {
11 Sweethearting {
13 cashier_id: String,
14 beneficiary: String,
15 estimated_loss: Decimal,
16 transaction_count: u32,
17 },
18 Skimming {
20 register_id: String,
21 store_id: String,
22 amount: Decimal,
23 detection_method: String,
24 },
25 RefundFraud {
27 transaction_id: String,
28 employee_id: String,
29 refund_amount: Decimal,
30 scheme_type: RefundFraudType,
31 },
32 ReceivingFraud {
34 po_id: String,
35 employee_id: String,
36 short_quantity: u32,
37 value: Decimal,
38 },
39 TransferFraud {
41 from_store: String,
42 to_store: String,
43 items_diverted: u32,
44 value: Decimal,
45 },
46 CouponFraud {
48 coupon_code: String,
49 not_presented: bool,
50 value: Decimal,
51 transaction_count: u32,
52 },
53 EmployeeDiscountAbuse {
55 employee_id: String,
56 non_employee_beneficiary: String,
57 discount_value: Decimal,
58 transaction_count: u32,
59 },
60 VoidAbuse {
62 cashier_id: String,
63 void_count: u32,
64 void_total: Decimal,
65 period_days: u32,
66 },
67 PriceOverrideAbuse {
69 employee_id: String,
70 override_count: u32,
71 total_discount: Decimal,
72 },
73 GiftCardFraud {
75 scheme_type: GiftCardFraudType,
76 amount: Decimal,
77 cards_affected: u32,
78 },
79 InventoryManipulation {
81 store_id: String,
82 manipulation_type: InventoryManipulationType,
83 value: Decimal,
84 },
85 VendorKickback {
87 vendor_id: String,
88 buyer_id: String,
89 kickback_amount: Decimal,
90 scheme_duration_days: u32,
91 },
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
96pub enum RefundFraudType {
97 RefundToPersonalCard,
99 FakeMerchandiseReturn,
101 NoReceiptFraud,
103 CrossRetailerFraud,
105 Wardrobing,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
111pub enum GiftCardFraudType {
112 LoadingWithoutPayment,
114 BalanceTransfer,
116 CardNumberHarvesting,
118 ReturnToGiftCard,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
124pub enum InventoryManipulationType {
125 PhantomInventory,
127 ConcealedShrinkage,
129 CountManipulation,
131 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 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 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}