@gameroom/cli
Version:
A command line tool for Gameroom
281 lines (264 loc) • 14.5 kB
JavaScript
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()
}
}