amazon-seller-fees
Version:
Utilities to fetch Amazon GST, referral and closing fees for sellers
382 lines (338 loc) • 11.5 kB
JavaScript
const { amazonClosingFee } = require("./amazonClosingFee");
const { amazonFeesData } = require("./amazonData");
const { amazonShippingFee } = require("./amazonShippingData");
// ---------------- Helper utilities ----------------
function normalizeText(t) {
return String(t || "")
.trim()
.toLowerCase();
}
function priceMatchesSlab(price, slab) {
const min =
typeof slab.min === "number" ? slab.min : Number.NEGATIVE_INFINITY;
const max =
typeof slab.max === "number" ? slab.max : Number.POSITIVE_INFINITY;
return price >= min && price <= max;
}
function findCategoryObj(categoryInput) {
if (!categoryInput) return null;
const q = normalizeText(categoryInput);
// direct match on id
const byId = amazonFeesData.categories.find(
(c) => normalizeText(c.category_id) === q
);
if (byId) return byId;
// match on category_name exact or contains
const byNameExact = amazonFeesData.categories.find(
(c) => normalizeText(c.category_name) === q
);
if (byNameExact) return byNameExact;
const byNameContains = amazonFeesData.categories.find((c) =>
normalizeText(c.category_name).includes(q)
);
if (byNameContains) return byNameContains;
// fallback: try token match (words)
const tokens = q.split(/\s+/).filter(Boolean);
if (tokens.length) {
const score = amazonFeesData.categories
.map((c) => {
const name = normalizeText(c.category_name);
const matches = tokens.reduce(
(acc, tk) => acc + (name.includes(tk) ? 1 : 0),
0
);
return { c, matches };
})
.sort((a, b) => b.matches - a.matches);
if (score[0] && score[0].matches > 0) return score[0].c;
}
return null;
}
function findSubcategoryObj(catObj, subcategoryInput) {
if (!catObj || !Array.isArray(catObj.subcategories) || !subcategoryInput)
return null;
const q = normalizeText(subcategoryInput);
// exact / id-like match
let found = catObj.subcategories.find((s) => normalizeText(s.name) === q);
if (found) return found;
// contains
found = catObj.subcategories.find((s) => normalizeText(s.name).includes(q));
if (found) return found;
// try token scoring
const tokens = q.split(/\s+/).filter(Boolean);
if (tokens.length) {
const score = catObj.subcategories
.map((s) => {
const name = normalizeText(s.name);
const matches = tokens.reduce(
(acc, tk) => acc + (name.includes(tk) ? 1 : 0),
0
);
return { s, matches };
})
.sort((a, b) => b.matches - a.matches);
if (score[0] && score[0].matches > 0) return score[0].s;
}
return null;
}
// ----------------- 1. getAllCategories -----------------
function getAllCategories() {
try {
const list = amazonFeesData.categories.map((c) => ({
category_id: c.category_id,
category_name: c.category_name,
}));
return { ok: true, categories: list };
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
// ----------------- 2. getSubcategories(category) -----------------
function getSubcategories(categoryInput) {
try {
const cat = findCategoryObj(categoryInput);
if (!cat) return { ok: false, error: "Category not found" };
const list = (cat.subcategories || []).map((s) => ({
name: s.name,
gst_percent: s.gst_percent,
}));
return {
ok: true,
category_id: cat.category_id,
category_name: cat.category_name,
subcategories: list,
};
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
// ----------------- 3. getGstPercent(category, subcategory) -----------------
function getGstPercent(categoryInput, subcategoryInput) {
try {
const cat = findCategoryObj(categoryInput);
if (!cat) return { ok: false, error: "Category not found" };
const sub = findSubcategoryObj(cat, subcategoryInput);
if (!sub) return { ok: false, error: "Subcategory not found" };
return {
ok: true,
gst_percent: typeof sub.gst_percent === "number" ? sub.gst_percent : null,
};
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
// ----------------- utility: compute fixed closing fee suggestions -----------------
function computeClosingFeesForPrice(price) {
const out = {};
const fixed = amazonClosingFee.fixedClosingFee || {};
Object.keys(fixed).forEach((key) => {
const val = fixed[key];
if (Array.isArray(val)) {
const slab = val.find((s) => priceMatchesSlab(price, s));
out[key] = slab ? slab.fee : null;
} else if (typeof val === "object" && val !== null) {
// selfShip has nested arrays like books / allExceptBooks
out[key] = {};
Object.keys(val).forEach((subkey) => {
const arr = val[subkey];
const slab = Array.isArray(arr)
? arr.find((s) => priceMatchesSlab(price, s))
: null;
out[key][subkey] = slab ? slab.fee : null;
});
} else {
out[key] = null;
}
});
return out;
}
// ----------------- 4. getClosingFee(category, subcategory, options) -----------------
function getClosingFee(categoryInput, subcategoryInput, options = {}) {
try {
const cat = findCategoryObj(categoryInput);
const sub = cat ? findSubcategoryObj(cat, subcategoryInput) : null;
const priceHint =
options && typeof options.price === "number" ? options.price : null;
if (typeof priceHint === "number") {
const allFees = computeClosingFeesForPrice(priceHint);
if (options.fulfillmentType) {
const ft = options.fulfillmentType;
// Special handling for selfShip (books vs others)
if (ft === "selfShip") {
const isBook =
normalizeText(subcategoryInput).includes("book") ||
normalizeText(cat?.category_name).includes("book");
const st = options.selfShipType || (isBook ? "books" : "allExceptBooks");
return {
ok: true,
fulfillmentType: ft,
selfShipType: st,
fee: allFees.selfShip ? allFees.selfShip[st] : null,
};
}
// For other fulfillment types (fba, easyShip, sellerFlex)
return {
ok: true,
fulfillmentType: ft,
fee: allFees[ft] || null,
};
}
// Return all slabs if fulfillmentType not specified
return { ok: true, fees_for_price: allFees };
}
// No price provided → return entire config
return { ok: true, fixedClosingFeeData: amazonClosingFee.fixedClosingFee };
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
// ----------------- 5. getReferralRate(category, subcategory, sellingPrice, options) -----------------
function getReferralRate(
categoryInput,
subcategoryInput,
sellingPrice,
options = {}
) {
try {
if (
typeof sellingPrice !== "number" ||
Number.isNaN(sellingPrice) ||
sellingPrice < 0
) {
return { ok: false, error: "Invalid sellingPrice" };
}
const cat = findCategoryObj(categoryInput);
if (!cat) return { ok: false, error: "Category not found" };
const sub = findSubcategoryObj(cat, subcategoryInput);
if (!sub) return { ok: false, error: "Subcategory not found" };
const slabs = Array.isArray(sub.referral_slabs) ? sub.referral_slabs : [];
// find matching slab
const slab = slabs.find((s) => priceMatchesSlab(sellingPrice, s)) || null;
const referral_percent = slab
? typeof slab.fee_percent === "number"
? slab.fee_percent
: null
: null;
const referralAmount =
referral_percent !== null
? (sellingPrice * referral_percent) / 100
: null;
const result = {
ok: true,
category_id: cat.category_id,
category_name: cat.category_name,
subcategory_name: sub.name,
gst_percent: typeof sub.gst_percent === "number" ? sub.gst_percent : null,
selling_price: sellingPrice,
referral_percent,
referral_amount: referralAmount,
};
// include closing fee suggestions if requested
if (options.includeClosingFee) {
const closingInfo = computeClosingFeesForPrice(sellingPrice);
if (options.fulfillmentType) {
if (options.fulfillmentType === "selfShip") {
const st =
options.selfShipType ||
(normalizeText(cat.category_name).includes("book")
? "books"
: "allExceptBooks");
result.closing_fee = {
fulfillmentType: "selfShip",
selfShipType: st,
fee: closingInfo.selfShip ? closingInfo.selfShip[st] : null,
};
} else {
result.closing_fee = {
fulfillmentType: options.fulfillmentType,
fee: closingInfo[options.fulfillmentType],
};
}
} else {
result.closing_fee_suggestions = closingInfo;
}
}
return result;
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
//========= Shipping Calculation ====
// example mode will be
// (
// fba
// easyShip
// selfShip
// sellerFlex
// )
// zone
// (
// local
// regional
// national
// )
function getAmazonShippingFee(mode, zone, weightKg) {
// Handle self-ship case
if (mode === "selfShip") {
return {
fee: 0,
// note: amazonShippingFee.selfShip.note
};
}
const slabs = amazonShippingFee[mode]?.[zone];
if (!slabs) {
throw new Error(`Invalid mode (${mode}) or zone (${zone})`);
}
// Find matching slab
for (let slab of slabs) {
if (slab.maxKg === null) {
// Above highest slab -> calculate base + extra
const extraWeight = Math.max(0, weightKg - slab.minKg);
const extraFee = Math.ceil(extraWeight) * slab.extraPerKg;
return {
fee: slab.base + extraFee,
// base: slab.base,
// extraFee
};
}
if (weightKg >= slab.minKg && weightKg <= slab.maxKg) {
return { fee: slab.fee };
}
}
throw new Error("No matching slab found for given weight");
}
// ----------------- Exports (for Node) -----------------
if (typeof module !== "undefined" && module.exports) {
module.exports = {
getAllCategories,
getSubcategories,
getGstPercent,
getClosingFee,
getReferralRate,
getAmazonShippingFee
};
}
// console.log("All Categories: \n\n", getAllCategories());
// console.log(
// "Subcategories for Electronics: \n\n",
// getSubcategories("Electronics")
// );
// console.log(
// "GST Percent for Electronics - Headphones: \n\n",
// getGstPercent("Electronics", "Headphones")
// );
// console.log(
// "Closing Fee for Books - Fiction: \n\n",
// getClosingFee("Books, Music, Movies, Video Games, Entertainment", "Books", {
// price: 450,
// fulfillmentType: "selfShip",
// })
// );
// console.log(
// "Referral Rate for Electronics - Headphones: \n\n",
// getReferralRate("Electronics", "Headphones", 150, {})
// );
// console.log(
// "Shipping Fee: \n\n",
// getAmazonShippingFee("easyShip", "national", 21)
// );