rankora-api / app /services /analytics /amazon_fees.py
Shoaib898's picture
Deploy Rankora API (clean bundle)
5459540 verified
Raw
History Blame Contribute Delete
8.91 kB
"""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)