You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

238 lines
11 KiB
JavaScript

const env = require('dotenv');
env.config();
const brickLinkAPI = require('./bricklink');
const brickowlAPI = require('./brickowl');
const _ = require('lodash');
const util = require('util');
const Cart = require('./Cart');
function getRandomizedDelay(){
return Math.random() * 500 + 500;
}
async function getListings(order) {
// const results = []
var promises = order.map(lot => {
return new Promise(async (resolve, reject) => {
try {
if(!lot.partNumber) {
const details = await brickowlAPI.getDetails(lot.internalPartId);
lot.colorId = details.color_id;
lot.partNumber = _.get(_.find(details.ids, (i => i.type === 'design_id')), 'id', null)
lot.desc = details.name
if(!lot.partNumber) {
console.log('Unable to get lego part number for ', lot.internalPartId)
lot.partNumber = details.name
}
}
// var listings = await brickowlAPI.getListingsByLegoPartNumber(lot.partNumber, lot.colorId);
var listings = await brickowlAPI.getListings(lot.internalPartId, lot.colorId);
console.log(listings.length + 'Listings found for', lot.internalPartId, `(#${lot.colorId}#)`);
const lotsWithEnoughItems = listings.filter(l => l.quantity >= lot.quantity);
// if(lotsWithEnoughItems.length === 0) return reject('Unable to source part ' + lot.blPartNumber)
resolve(lotsWithEnoughItems.map(lota => {
//update the quantity to only what we need
lota.quantity = lot.quantity;
return lota;
}));
//Wait added to not cause a 403. dont want to dos bricklink
// await new Promise(resolve => setTimeout(resolve, getRandomizedDelay()));
} catch (error) {
console.error('Unable to source: ', lot.blPartNumber, error)
reject(error);
}
})
});
// for (var i = 0; i < order.length; i++) {
// const lot = order[i];
// }
return await Promise.all(promises);
}
brickowlAPI.getSetInventory('2996-1').then((inventory) => {
// console.log('Inventory', inventory)
getListings(inventory).then(async parts => {
var carts = [];
const possibleLots = _.flatten(parts);
possibleLots.forEach(lot => {
let logging = false;
const group = _.findIndex(carts, l => l.sellerUsername === lot.sellerUsername);
if (group === -1) {
const newGroup = new Cart(lot.sellerUsername, lot.minBuy, [lot]);
carts.push(newGroup);
} else {
carts[group].addToCart(lot);
}
})
//sometimes sellers make multiple listings of the same part in the same color. filter for the cheapest
carts = carts.map(cart => {
const groupedLots = _.groupBy(cart.lots, (lot) => `${lot.blPartNumber}-${lot.colorId}`);
const listingsToRemove = [];
_.keys(groupedLots).forEach((partType) => {
if (groupedLots[partType].length <= 1) return;
const pricesPerLot = groupedLots[partType].map((lot) => lot.getPrice(lot.quantity));
const minPrice = Math.min(...pricesPerLot);
const lotsAtMinPrice = groupedLots[partType].filter((lot) => lot.getPrice(lot.quantity) === minPrice);
if (lotsAtMinPrice.length === 1) {
listingsToRemove.push(..._.difference(groupedLots[partType], ...lotsAtMinPrice))
return;
}
//Just keep the first one at this point. they're the same price.
listingsToRemove.push(...groupedLots[partType].slice(1));
})
if(listingsToRemove.length > 0) console.log(`Removing ${listingsToRemove.length} listings for duplication from seller ${cart.sellerUsername}....`);
return new Cart(
cart.sellerUsername,
cart.minBuy,
_.differenceBy(cart.lots, listingsToRemove, (a) => a.listingId))
})
const fs = require('fs');
await fs.writeFileSync('./carts.json', JSON.stringify(carts))
//throw out any carts that won't meet min buy
var aboveMinBuy = carts.filter((cart) => cart.getPrice() >= cart.minBuy);
console.log(`Threw out ${carts.length - aboveMinBuy.length} carts due to min purchase requirements.`)
console.log(`Found ${aboveMinBuy.length} possible carts`);
//brute force things here for now
//generage every possible order combo
//for every cart, see if we have everything.
var currentlyUnsourcedParts = inventory;
var calculatedCarts = [];
while (currentlyUnsourcedParts.length > 0) {
console.log(`Searching for parts to cover ${currentlyUnsourcedParts.length} lots...`)
const bestAddition = getCartWithBestCoverage(aboveMinBuy, currentlyUnsourcedParts);
if(!bestAddition) {
//run out of coverage options
break;
}
calculatedCarts.push(bestAddition);
console.log(`Added new cart to cover ${bestAddition.lots.length} lots for ${bestAddition.getPrice()}`);
//check for duplicated lots across carts and keep the lowest price one, so long as it doesn't push us below min buy
const lotsFromAllCarts = calculatedCarts.reduce((acc, cart) => { acc.push(...cart.lots); return acc; }, [])
const groupedLots = _.groupBy(lotsFromAllCarts, (lot) => `${lot.blPartNumber}`);
const listingsToRemove = [];
_.keys(groupedLots).forEach((partType) => {
const legoPartNumber = partType.split('-')[0];
const colorId = partType.split('-')[1];
if (groupedLots[partType].length <= 1) return;
const pricesPerLot = groupedLots[partType].map((lot) => lot.getPrice(lot.quantity));
const minPrice = Math.min(...pricesPerLot);
const lotsAtMinPrice = groupedLots[partType].filter((lot) => lot.getPrice(lot.quantity) === minPrice);
if (lotsAtMinPrice.length === 1) {
//TODO possibly perform check to see if we're gonna push the others below min buy
listingsToRemove.push(...(_.difference(groupedLots[partType], lotsAtMinPrice).map(i => i.listingId)))
return;
}
//everything is the same price here, lets just keep the one in the largest lot
const cartSizesPerLot = lotsAtMinPrice.map(lot => {
//what cart do I belong to?
const parentCart = calculatedCarts.find((cart) => cart.hasListingId(lot.listingId));
return parentCart.lots.length;
});
const largestCartSize = Math.max(...cartSizesPerLot);
const cartsAtLargestSize = calculatedCarts.filter(cart => cart.lots.length === largestCartSize);
if (cartsAtLargestSize.length === 1) {
//keep this cart, remove the item from the others.
//get the listing in this cart
const cart = cartsAtLargestSize[0];
const keepingLot = cart.lots.find((lot) => lot.blPartNumber == partType);
// console.log(cart, keepingLot, legoPartNumber, colorId)
listingsToRemove.push(...(groupedLots[partType].filter(lot => lot.listingId !== keepingLot.listingId).map(i => i.listingId)))
return;
}
//at this point, they're same price, and same cart sizes. Just pick the first one
listingsToRemove.push(...groupedLots[partType].slice(1).map(i => i.listingId));
})
calculatedCarts.forEach(cart => {
listingsToRemove.forEach(listingId => cart.removeListingIdFromCart(listingId))
});
currentlyUnsourcedParts = getUnsourcedParts(calculatedCarts, currentlyUnsourcedParts);
}
// console.log('Final cart', util.inspect(calculatedCarts, false, null, true));
console.log('Total price without shipping: ', calculatedCarts.reduce((acc, cart) => acc + cart.getPrice(), 0), 'across', calculatedCarts.length, 'stores');
if(currentlyUnsourcedParts.length > 0) {
console.log('Some lots couldnt be sourced from a single seller', currentlyUnsourcedParts)
}
calculatedCarts.forEach((cart) => console.log(cart.toString()));
})
})
function getCartWithBestCoverage(carts, partsList) {
var coverages = carts.map(cart => {
const itemsNotInThisCart = partsList.filter(item => {
return _.findIndex(cart.lots, lot => lot.blPartNumber === item.internalPartId && lot.colorId === item.colorId) === -1;
})
if (itemsNotInThisCart.length === 0) {
//ONE STORE SOLUTION FOUND, NICE!
// console.log('Coverage found: ', cart);
return 0;
}
return itemsNotInThisCart.length;
})
if (coverages.reduce((acc, val) => acc + val, 0) === 0) {
}
const bestCoverage = Math.min(...coverages);
const coverageOptions = carts.filter((value, index, array) => {
return coverages[index] === bestCoverage
}).sort((a, b) => a.getPrice() - b.getPrice());
if(bestCoverage === partsList.length){
//no carts are providing coverage.
console.log('Lacking coverage for the following parts', partsList)
// throw new Error('Cannot fulfill order, lacking coverage');
return false;
}
// if(Math.min(...coverages)) === )
// const bestCoverageIndex = coverages.indexOf();
return coverageOptions[0];
}
function getUnsourcedParts(carts, partsList) {
const allLotsFromAllCarts = _.uniqBy(carts.reduce((acc, cart) => { acc.push(...cart.lots); return acc; }, []), (lot) => `${lot.blPartNumber}_${lot.colorId}`);
console.log(allLotsFromAllCarts.map(a => a.blPartNumber).join(','));
return partsList.filter((part) => {
const coverageIndex = allLotsFromAllCarts.findIndex((lot) => {
if(lot.blPartNumber === "868124-58")
console.log(lot, part)
return lot.blPartNumber === part.internalPartId && (!lot.colorId || lot.colorId === part.colorId)
})
if(coverageIndex !== -1) console.log(`Coverage for ${part.internalPartId} found!`)
return coverageIndex === -1
})
}