"""Amazon US marketplace fee formulas (Seller Central Revenue Calculator aligned). Sources: Amazon Seller Central fee schedule (FBA fulfillment, referral, storage, inbound placement). Rates reflect 2024–2025 US standard-size tiers. """ from __future__ import annotations from dataclasses import dataclass from typing import Dict, Literal, Optional, Tuple Season = Literal["standard", "peak"] # Referral fee % by category keyword (Amazon US published rates) REFERRAL_RATES: Dict[str, float] = { "amazon device": 0.45, "apparel": 0.17, "clothing": 0.17, "shoe": 0.15, "jewelry": 0.20, "electronics": 0.08, "computer": 0.08, "camera": 0.08, "cell phone": 0.08, "cell phones": 0.08, "phone": 0.08, "wireless": 0.08, "tablet": 0.08, "beauty": 0.08, "personal care": 0.08, "health": 0.08, "grocery": 0.08, "baby": 0.08, "book": 0.15, "music": 0.15, "video": 0.15, "software": 0.15, "video game": 0.15, "game console": 0.08, "toy": 0.15, "home": 0.15, "kitchen": 0.15, "garden": 0.15, "tool": 0.15, "sport": 0.15, "outdoor": 0.15, "pet": 0.15, "office": 0.15, "industrial": 0.12, "automotive": 0.12, "default": 0.15, } REFERRAL_MIN = 0.30 # 3.5% fuel & logistics surcharge on FBA fulfillment (US/CA, from Apr 2026) FULFILLMENT_LOGISTICS_SURCHARGE = 1.035 # FBA US fulfillment — large standard non-apparel (2025–2026 US, before surcharge in table) # Weight in oz → fee USD (small standard ≤16 oz, large standard ≤20 lb) SMALL_STANDARD_OZ: Tuple[Tuple[float, float], ...] = ( (2, 3.06), (4, 3.15), (6, 3.24), (8, 3.33), (10, 3.43), (12, 3.53), (14, 3.60), (16, 3.65), ) LARGE_STANDARD_LBS: Tuple[Tuple[float, float], ...] = ( (0.25, 4.99), (0.5, 5.28), (0.75, 5.48), (1.0, 5.93), (1.25, 6.38), (1.5, 6.62), (1.75, 6.98), (2.0, 7.20), (2.25, 7.46), (2.5, 7.72), (2.75, 7.95), (3.0, 8.38), (20.0, 11.90), ) # Storage $/cu ft per month (standard-size) STORAGE_RATE_CUFT: Dict[Season, float] = { "standard": 0.87, # Jan–Sep "peak": 2.40, # Oct–Dec } # Default unit volume when dimensions unknown (typical small standard ~0.05 cu ft) DEFAULT_CU_FT_PER_UNIT = 0.05 # Inbound placement service fee per unit (US) — applied when seller enters inbound shipping INBOUND_PLACEMENT_DEFAULT = 0.56 INBOUND_BY_REGION = { "west": 0.56, "central": 0.48, "east": 0.52, } REMOVAL_FEE_PER_UNIT = 3.95 DISPOSAL_FEE_PER_UNIT = 3.95 def calculate_removal_disposal_cost( removal_units: int = 0, disposal_units: int = 0, monthly_units_sold: float = 1.0, ) -> Dict: total = round(removal_units * REMOVAL_FEE_PER_UNIT + disposal_units * DISPOSAL_FEE_PER_UNIT, 2) per_unit = round(total / max(monthly_units_sold, 1), 2) if total else 0.0 return { "removal_fee_per_unit": REMOVAL_FEE_PER_UNIT, "disposal_fee_per_unit": DISPOSAL_FEE_PER_UNIT, "removal_units": removal_units, "disposal_units": disposal_units, "total_removal_disposal_fee": total, "removal_disposal_cost_per_unit_sold": per_unit, } @dataclass class ProductDimensions: weight_lbs: float = 1.0 length_in: float = 10.0 width_in: float = 8.0 height_in: float = 2.0 @property def weight_oz(self) -> float: return self.weight_lbs * 16 @property def cubic_feet(self) -> float: return max((self.length_in * self.width_in * self.height_in) / 1728, 0.001) def resolve_referral_rate(category: str) -> Tuple[float, str]: cat = (category or "").lower() # Cell phones & accessories → 8% (must check before generic "accessories") if "cell phone" in cat or "cell phones" in cat or ("phone" in cat and "accessories" in cat): return 0.08, "cell phones" for key, rate in REFERRAL_RATES.items(): if key != "default" and key in cat: return rate, key return REFERRAL_RATES["default"], "default" def _uses_large_standard(dims: ProductDimensions, category: str = "") -> bool: """Phones and many electronics ship in large-standard boxes even under 1 lb.""" cat = (category or "").lower() if any(k in cat for k in ("cell phone", "phone", "tablet", "electronics", "computer")): return True longest = max(dims.length_in, dims.width_in, dims.height_in) median = sorted([dims.length_in, dims.width_in, dims.height_in])[1] if longest > 15 or median > 12 or dims.weight_lbs > 1.0: return True return dims.weight_lbs * 16 > 16 def calculate_fba_fulfillment_fee(weight_lbs: float, dims: Optional[ProductDimensions] = None, category: str = "") -> Dict: """Return FBA pick-pack-ship fee from weight/size tier + 3.5% logistics surcharge.""" if dims is None: dims = estimate_dimensions_from_weight(weight_lbs, category) weight_lbs = max(weight_lbs, dims.weight_lbs) weight_oz = weight_lbs * 16 use_large = _uses_large_standard(dims, category) fee = 11.90 tier = "large_standard" if use_large else "small_standard" if not use_large and weight_oz <= 16: for max_oz, tier_fee in SMALL_STANDARD_OZ: if weight_oz <= max_oz: fee = tier_fee break else: tier = "large_standard" for max_lbs, tier_fee in LARGE_STANDARD_LBS: if weight_lbs <= max_lbs: fee = tier_fee break base_fee = round(fee, 2) total_fee = round(base_fee * FULFILLMENT_LOGISTICS_SURCHARGE, 2) return { "fba_fulfillment_fee": total_fee, "fba_fulfillment_fee_base": base_fee, "fulfillment_surcharge_percent": 3.5, "fulfillment_tier": tier, "weight_lbs": round(weight_lbs, 3), "weight_oz": round(weight_oz, 1), } def calculate_referral_fee(price: float, category: str = "default") -> Dict: rate, matched = resolve_referral_rate(category) fee = round(max(price * rate, REFERRAL_MIN), 2) return { "referral_fee": fee, "referral_rate_percent": round(rate * 100, 1), "referral_category": matched, "fixed_closing_fee": 0.0, "variable_closing_fee": 0.0, "digital_services_fee": 0.0, } def calculate_storage_fee_per_unit( dims: ProductDimensions, season: Season = "standard", avg_inventory_units: float = 1.0, monthly_units_sold: float = 1.0, ) -> Dict: monthly_per_cuft = STORAGE_RATE_CUFT[season] monthly_per_unit = round(dims.cubic_feet * monthly_per_cuft, 4) if monthly_units_sold <= 0: per_unit_sold = 0.0 else: per_unit_sold = round((monthly_per_unit * max(avg_inventory_units, 0)) / monthly_units_sold, 2) return { "season": season, "cubic_feet_per_unit": round(dims.cubic_feet, 4), "monthly_storage_rate_per_cuft": monthly_per_cuft, "monthly_storage_cost_per_unit": monthly_per_unit, "avg_inventory_units": avg_inventory_units, "monthly_units_sold": monthly_units_sold, "storage_cost_per_unit_sold": per_unit_sold, } def calculate_inbound_fees( region: str = "west", shipping_cost_per_shipment: float = 0.0, units_per_shipment: int = 1, ) -> Dict: region_key = (region or "west").lower() placement = INBOUND_BY_REGION.get(region_key, INBOUND_PLACEMENT_DEFAULT) shipping_per_unit = round(shipping_cost_per_shipment / max(units_per_shipment, 1), 2) if shipping_cost_per_shipment > 0 else 0.0 total = round(placement + shipping_per_unit, 2) return { "inbound_region": region_key, "inbound_placement_fee_per_unit": placement, "shipping_cost_per_unit": shipping_per_unit, "total_inbound_cost_per_unit": total, } def estimate_dimensions_from_weight(weight_lbs: float, category: str = "") -> ProductDimensions: """Estimate package size from weight/category when scrape has no dimensions.""" cat = (category or "").lower() if any(k in cat for k in ("cell phone", "phone", "tablet")): # Typical retail phone box (~0.11 cu ft) aligns with Seller Central storage line return ProductDimensions(weight_lbs=weight_lbs, length_in=9.0, width_in=6.5, height_in=3.5) if weight_lbs <= 1: return ProductDimensions(weight_lbs=weight_lbs, length_in=10, width_in=8, height_in=2) if weight_lbs <= 3: return ProductDimensions(weight_lbs=weight_lbs, length_in=14, width_in=10, height_in=4) return ProductDimensions(weight_lbs=weight_lbs, length_in=18, width_in=12, height_in=6)