Spaces:
Running
Running
| """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, | |
| } | |
| class ProductDimensions: | |
| weight_lbs: float = 1.0 | |
| length_in: float = 10.0 | |
| width_in: float = 8.0 | |
| height_in: float = 2.0 | |
| def weight_oz(self) -> float: | |
| return self.weight_lbs * 16 | |
| 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) | |