UNPKG

@gameroom/cli

Version:

A command line tool for Gameroom

281 lines (264 loc) 14.5 kB
const cosmetic = require('cosmetic'), { join } = require('path'), { enums: { sale_status }, models: { Address, Container, Image, Line, Payment, Price, Product, Sale, Store, Unit, Unit_Extended } } = require('@gameroom/kit'), { componentString, getAll, grGreen, timeout, writeCSVFile } = require('../../helpers'), { Shopify } = require('../../models'), { config, conversions, google_product_categories, spinner } = require('../../refs'), LIMIT = 500, SHOPIFY_LIMIT = 250 module.exports = async ({ verbose }) => { // catch all errors that arent 429 (too many requests) or 404 (not found) try { config.exit_gracefully = true const dev = process.env.NODE_ENV === 'development' const SHOPIFY_STORE_URL = dev ? process.env.SHOPIFY_DEV_STORE_URL : process.env.SHOPIFY_STORE_URL if (!SHOPIFY_STORE_URL) throw new Error('missing shopify env keys') spinner.info(`updating ${cosmetic.green(SHOPIFY_STORE_URL)} @ ${new Date().toLocaleString()}`) // stores spinner.text = `getting ${grGreen('stores')}` const stores = await Store.getAll() spinner.info(`got ${stores.length} ${grGreen('stores')}`) // store addresses spinner.text = `getting ${grGreen('addresses')}` const address_filters = [] for (const store of stores) address_filters.push({ key: 'addressable_id', value: store.id }) const addresses = await Address.getAll({ filter: { or: address_filters } }) spinner.info(`got ${addresses.length} ${grGreen('addresses')}`) // containers spinner.text = `getting ${grGreen('containers')}` const containers = await Container.getAll() spinner.info(`got ${containers.length} ${grGreen('containers')}`) // shopify locations spinner.text = `getting ${cosmetic.green('locations')}` const locations = await Shopify.Location.get() spinner.info(`got ${locations.length} ${cosmetic.green('locations')}`) // Gameroom units >> Shopify products // get units and operate in chunks of 500 // let api manage updated_at // units will be recieved again for updates // save units in an object to reference for updates when updating shopify // just save id: updated_at in objects // compare updated_at dates // could delete object[id] if i want to // date filter let filter if (config.last_shopify_update) { spinner.info(`last update ${new Date(config.last_shopify_update).toLocaleString()}`) const date = new Date(config.last_shopify_update) // just in case... // date.setTime(date.getTime() - 1) filter = { key: 'updated_at', comparison: '>', value: new Date(config.last_shopify_update) } } else { spinner.info(`first shopify update`) } // Get updated units and add or update or remove them on shopify let offset = 0, done = false, processed = {} // update shopify let added = 0, removed = 0, skipped = 0, updated = 0, not_offered = 0, current = 0, total = 0 spinner.text = `0 / 0 processed, 0 ${cosmetic.green('added')}, 0 ${cosmetic.red('removed')}, 0 ${cosmetic.yellow('updated')}, 0 skipped, 0 not offered` while (!done) { if (config.should_exit) break // get batch of units const units = await Unit_Extended.get({ filter, limit: LIMIT, offset, sort: [{ updated_at: 1 }] }) if (units.length < LIMIT) done = true offset += LIMIT total += units.length if (verbose) spinner.info(`got ${units.length} ${grGreen('units')}`) // iterate through batch for (let [i, unit] of units.entries()) { try { if (config.should_exit) break // snapshot unit in case it needs updated const shopified = unit.shopified // orphan and shopifyable checks if (!unit.store_id && verbose) spinner.warn(`${grGreen('unit')} ${unit.id} has no ${grGreen('store')}`) if (!unit.container_id && verbose) spinner.warn(`${grGreen('unit')} ${unit.id} has no ${grGreen('container')}`) if (!unit.product_id && verbose) spinner.warn(`${grGreen('unit')} ${unit.id} has no ${grGreen('product')}`) if (!unit.price_id && verbose) spinner.warn(`${grGreen('unit')} ${unit.id} has no ${grGreen('price')}`) if (!unit.image && verbose) spinner.warn(`${grGreen('unit')} ${unit.id} has no ${grGreen('image')}`) // find unit's store on shipify 'location' const store = unit.store_id ? stores.find(s => s.id === unit.store_id) : null const store_address = store ? addresses.find(a => a.addressable_id === store.id) : null const street_number = store_address ? store_address.street1.split(' ')[0] : null const location = street_number ? locations.find(l => l.address1.includes(street_number)) : null if (!location && verbose) spinner.warn(`${grGreen('unit')} ${unit.id} has no ${cosmetic.green('location')}`) // find unit's container const container = unit.container_id ? containers.find(i => i.id === unit.container_id) : null // values required to be shopified const shopifyable = store && store.offered && container && container.offered && unit.offered && unit.image && unit.amount > 0 && unit.quantity > 0 && location // get shopify product by handle let product = await Shopify.Product.getWithHandle(unit.id) // conditions const add = !product && shopifyable const remove = product && !shopifyable const update = product && shopifyable // snapshot unit last updated_at const { updated_at } = unit if (add) { // create shopify product, variant, and image product = await Shopify.Product.fromUnitExtended(unit) product = await product.save() // if (verbose) spinner.succeed(`created shopify ${cosmetic.green('product')} for ${grGreen('unit')} ${unit.id}`) // create shopify inventory const inventory_level = new Shopify.Inventory_Level({ available: unit.quantity, inventory_item_id: product.variants[0].inventory_item_id, location_id: location.id }) await inventory_level.save() // if (verbose) spinner.succeed(`created shopify ${cosmetic.green('iventory_level')} for ${grGreen('unit')} ${unit.id}`) // update gameroom unit unit.shopified = true // updated units go to the end of the pagination line and mess up the offset... offset-- if (verbose) spinner.succeed(`created shopify ${cosmetic.green('product')} and ${cosmetic.green('iventory_level')} for ${grGreen('unit')} ${unit.id}`) added++ } else if (remove) { // delete shopify product await Shopify.Product.delete(product.id) // update gameroom unit unit.shopified = false // updated units go to the end of the pagination line and mess up the offset... offset-- if (verbose) spinner.succeed(`removed shopify ${cosmetic.green('product')} for ${grGreen('unit')} ${unit.id}`) removed++ } else if (update) { // check updated_at if (processed[unit.id] === unit.updated_at) { config.last_shopify_update = new Date(unit.updated_at * 1000) delete processed[unit.id] continue } // update shopify product product = await product.updateFromUnitExtended(unit) // update shopify inventory const inventory_levels = await Shopify.Inventory_Level.get(product.variants[0].inventory_item_id) // move or create inventory item let inventory_level = inventory_levels.find(l => l.location_id === location.id) if (!inventory_level) inventory_level = new Shopify.Inventory_Level({ inventory_item_id: product.variants[0].inventory_item_id, location_id: location.id }) if (inventory_level.available !== unit.quantity) { // all new and updated inventory_level.available = unit.quantity await inventory_level.save() } // more than 1 inventory level shouldnt be possible // delete old inventory levels for (const l of inventory_levels.filter(l => l.location_id !== location.id)) await Shopify.Inventory_Level.delete(l) // update gameroom unit if not shopified unit.shopified = true // if (verbose) spinner.succeed(`${grGreen('unit')} ${unit.id} updated`) if (verbose) spinner.succeed(`updated shopify ${cosmetic.green('product')} and ${cosmetic.green('inventory_level')} for ${grGreen('unit')} ${unit.id}`) updated++ } else { if (unit.offered) { if (verbose) spinner.warn(`${grGreen('unit')} ${unit.id} skipped`) skipped++ } else { not_offered++ } // check for other mistakes // unshopify unit if it has no shopify product if (!product && unit.shopified) { unit.shopified = false if (verbose) spinner.succeed(`${grGreen('unit')} ${unit.id} updated`) } } if (unit.shopified !== shopified) { // update unit if need be unit = await Unit.update({ id: unit.id, shopified: unit.shopified }) if (verbose) spinner.succeed(`updated ${grGreen('unit')} ${unit.id}`) } config.last_shopify_update = new Date(updated_at * 1000) // spinner.info(new Date(unit.updated_at * 1000).toLocaleString()) processed[unit.id] = unit.updated_at current++ spinner.text = `${current} / ${total} processed, ${added} ${cosmetic.green('added')}, ${removed} ${cosmetic.red('removed')}, ${updated} ${cosmetic.yellow('updated')}, ${skipped} skipped, ${not_offered} not offered` } catch (err) { current++ skipped++ spinner.fail(`failed on ${grGreen('unit')} ${unit.id}: ${err}`) } } } spinner.succeed(`${total} processed, ${added} ${cosmetic.green('added')}, ${removed} ${cosmetic.red('removed')}, ${updated} ${cosmetic.yellow('updated')}, ${skipped} skipped, ${not_offered} not offered`) // Shopify orders >> Gameroom sales let order_count = 0, sales = 0, lines = 0, taxes = 0, payments = 0 updated = 0, offset = 0, done = false // get shopify orders, create a sale, lines, payment, and update units while (!done) { spinner.text = `${order_count} ${cosmetic.green('orders')}, ${sales} ${grGreen('sales')}, ${lines} ${grGreen('lines')}, ${taxes} ${grGreen('taxes')}, ${payments} ${grGreen('payments')}, ${updated} ${grGreen('units')} updated` const orders = await Shopify.Order.get({ limit: SHOPIFY_LIMIT, since_id: config.last_shopify_id || null }) if (verbose) spinner.info(`got ${orders.length} ${cosmetic.green('orders')}`) if (orders.length < SHOPIFY_LIMIT) done = true // for each order for (const order of orders) { if (config.should_exit) break // create sale const sale = await Sale.create({ info: `shopify order number: ${order.order_number}`, status: sale_status.ordered }) if (verbose) spinner.succeed(`created ${grGreen('sale')}: ${sale.id}`) sales++ // for each order line_item for (const line_item of order.line_items) { spinner.text = `${order_count} ${cosmetic.green('orders')}, ${sales} ${grGreen('sales')}, ${lines} ${grGreen('lines')}, ${taxes} ${grGreen('taxes')}, ${payments} ${grGreen('payments')}, ${updated} ${grGreen('units')} updated` // if line item has no shopify product id continue if (!line_item.product_id) { spinner.warn(`shopify missing ${cosmetic.green('product_id')} for ${cosmetic.green('order')}: ${order.order_number}`) continue } // get shopify product const product = await Shopify.Product.getWithId(line_item.product_id) if (!product) continue // get unit let unit try { unit = await Unit.find(product.handle) } catch(err) {/*Not found*/} if (!unit) continue // update unit const quantity = unit.quantity - line_item.quantity const updates = { id: unit.id, quantity } if (quantity <= 0) { // updates.advertised = false updates.offered = false updates.shopified = false } await Unit.update(updates) if (verbose) spinner.succeed(`updated ${grGreen('unit')}: ${unit.id}`) updated++ // create line const line = await Line.create({ amount: Math.floor(parseFloat(line_item.price) * 100), name: line_item.name, quantity: line_item.quantity, sale_id: sale.id, saleable_id: unit.id, saleable_type: 'Unit', info: `shopify product id: ${line_item.id}` }) if (verbose) spinner.succeed(`created ${grGreen('line')}: ${line.id}`) lines++ } // create tax line const tax_amount = Math.floor(parseFloat(order.total_tax) * 100) if (tax_amount) { const tax = await Line.create({ name: 'Tax', quantity: 1, sale_id: sale.id, tax: tax_amount, info: `shopify order tax` }) taxes++ } // create payment const payment_amount = Math.floor(parseFloat(order.subtotal_price) * 100) const payment = await Payment.create({ amount: payment_amount, gateway: 'online', reference: `shopify_${order.id}#${order.order_number}`, sale_id: sale.id }) if (verbose) spinner.succeed(`created ${grGreen('payment')}: ${payment.id}`) payments++ // done if (verbose) spinner.succeed(`completed ${cosmetic.green('order')}: ${order.id} #${order.order_number}`) config.last_shopify_id = order.id order_count++ } } spinner.succeed(`${order_count} ${cosmetic.green('orders')}, ${updated} ${grGreen('units')} updated`) spinner.succeed(`completed ${cosmetic.green('shopify')} update @ ${new Date().toLocaleString()}`).stop() } catch(err) { spinner.fail(`error updating ${cosmetic.green('shopify')}: ${err}`).stop() } }