const pg = require('../pg') const joinjs = require('join-js').default; const debug = require('debug')('sos:db:order') const mappings = require('../mappings') const dbUtil = require('../util') const config = require('./config') const {DateTime} = require('luxon') const easypost = new (require('@easypost/api'))(process.env.EASYPOST_API_KEY); const order = module.exports = {} order.create = async function(cart_uuid, session_uuid){ const query = { text: 'select * from sos.create_order($1, $2)', values: [cart_uuid, session_uuid] } debug(query); const {rows} = await pg.query(query) return joinjs.map(rows, mappings, 'orderMap', 'order_')[0]; } order.findForCart = async function(cart_uuid) { const query = { text: 'select * from sos.find_order_for_cart($1)', values: [cart_uuid] } debug(query) const {rows} = await pg.query(query) return joinjs.map(rows, mappings, 'orderMap', 'order_')[0]; } order.findAllForUser = async (user_uuid) => dbUtil.executeFunction({ name: 'find_orders_for_user', params: [ user_uuid ], returnType: 'order', single: false }) order.setUser = (order_uuid, user_uuid) => dbUtil.executeFunction({ name: 'set_user_on_order', params: [ order_uuid, user_uuid ], returnType: 'order', single: true }) order.findAllForSession = async function(session_uuid) { const query = { text: 'select * from sos.find_orders_for_session($1)', values: [session_uuid] } debug(query) const {rows} = await pg.query(query) return joinjs.map(rows, mappings, 'orderMap', 'order_'); } order.findForTransaction = transaction_uuid => dbUtil.executeFunction({ name: 'find_order_for_transaction', params: [ transaction_uuid ], returnType: 'order', single: true }) // TODO: Perhaps pagination? order.findAllComplete = () => dbUtil.executeQuery({ query: 'select * from sos.v_order where transaction_payment_state = \'completed\'', returnType: 'order', single: false }) order.findByUUID = uuid => dbUtil.executeQuery({ query: { text: 'select * from sos.v_order where order_uuid = $1', values: [uuid] }, returnType: 'order', single: true }) order.addAddress = async function (transaction, address){ // Get parcel size const parcel = { weight: .2 * transaction.cart.items.length, width: 10, length: transaction.cart.items.length, height: 3 } // Create shipment const epShipment = new easypost.Shipment({ to_address: address.easypost_id, from_address: { street1: '11381 N. Sampson Drive', city: 'Highland', state: 'UT', zip: '84003', country: 'US' }, parcel }) await epShipment.save() // Get shipping price (as cents) const lowestRate = epShipment.lowestRate(['USPS']) const price = parseFloat(lowestRate && lowestRate.retail_rate) * 100 if(!price) throw new Error("Unable to estimate price"); // Add up tax for all the items const tax = epShipment.to_address.state !== 'UT' ? null : transaction.cart.items // For each item type, create an array of length n (number of items of that type in cart) // where each index is initialized to the item tax amount .map(({item, count}) => new Array(count).fill(item.tax_rate * item.price_cents / 100)) // Flatten to just be all tax amounts .flat() // Sum all the numbers .reduce((a,c) => (a+c)) // Update database const query = { text: 'select * from sos.add_address_to_order($1, $2, $3)', values: [transaction.uuid, address.uuid, price] } debug(query) await pg.query(query) // Update tax return await order.updateTax(transaction.uuid) } order.updateTax = async function(transaction_uuid){ const _order = await order.findForTransaction(transaction_uuid) if(!_order.address){ debug("Skipping tax because order has no address yet") return _order } if(_order.address.state !== 'UT'){ debug("Skipping tax for state: " + _order.address.state); return _order } const {item_total_price, coupon_effective_discount} = _order.transactions.find(t => t.uuid === transaction_uuid) const itemPriceWithDiscount = item_total_price - (coupon_effective_discount || 0) const taxRate = await config.getTaxRate() const computedTax = Math.round(itemPriceWithDiscount * taxRate / 100) return await dbUtil.executeFunction({ name: 'add_tax_to_transaction', params: [ transaction_uuid, computedTax ], returnType: 'order', single: true }) } order.addCoupon = async function(transaction, coupon) { // Check coupon validity const couponExpireTime = DateTime.fromJSDate(coupon.valid_until) if(couponExpireTime.diffNow().as('seconds') < 0) throw new Error("Coupon has expired"); const {item_total_price} = transaction let discount = 0; if(coupon.flat_discount_cents) discount = coupon.flat_discount_cents; if(coupon.percent_discount) discount = Math.ceil(item_total_price * coupon.percent_discount / 100) if(coupon.per_sock_discount_cents) discount = transaction.cart.items // For each item type, create an array of length n (number of items of that type in cart) // where each index is initialized to the item price .map(({item, count}) => new Array(count).fill(item.price_cents)) // Flatten to just be all prices .flat() // For each price, convert it to the per-item discount (max of item price) .map(price => Math.min(price, coupon.per_sock_discount_cents)) // Sum all the discounts .reduce((a,c) => (a+c)) if(coupon.number_of_socks_free) discount = transaction.cart.items // For each item type, create an array of length n (number of items of that type in cart) // where each index is initialized to the item price .map(({item, count}) => new Array(count).fill(item.price_cents)) // Flatten to just be all prices .flat() // Order so the most expensive are at the front .sort((a,b) => (b-a)) // Keep only first n items .slice(0, coupon.number_of_socks_free) // Sum the item costs for total discount .reduce((a,c) => (a+c)) await dbUtil.executeFunction({ name: 'add_coupon_to_transaction', params: [ transaction.uuid, coupon.uuid, discount ], returnType: 'order', single: true }) // Update tax return await order.updateTax(transaction.uuid) } order.addPayment = async function(transaction, paymentIntent){ const [charge] = paymentIntent.charges.data if(!charge.paid) throw new Error("Charge was not paid") // TODO: Put these both in a common transaction. Flag purchase for follow-up // and/or send alert and/or refund on Stripe upon failure. const order = await dbUtil.executeFunction({ name: 'add_stripe_payment_to_transaction', params: [ transaction.uuid, paymentIntent.amount_received, paymentIntent.id, paymentIntent.receipt_email, paymentIntent.charges.data[0].receipt_number ], returnType: 'order', single: true }) await dbUtil.executeFunction({ name: 'deduct_stock_for_purchase', params: [transaction.uuid], returnType: 'item', single: false }) return order } order.setTracking = (uuid, trackingCode, shipDate, price_cents) => dbUtil.executeFunction({ name: 'set_delivery_tracking', params: [uuid, trackingCode, shipDate, price_cents], returnType: 'order', single: true }) order.setDelivery = (uuid, description, deliveryDate) => dbUtil.executeFunction({ name: 'set_delivery_by_hand', params: [uuid, description, deliveryDate], returnType: 'order', single: true }) order.shipEasyPost = async ( uuid, length, width, height, weight ) => { // Retrieve address const {address} = await order.findByUUID(uuid) const {shipping_from} = await config.getLatestConfig() if(!shipping_from?.easypost_id) throw new Error("Cannot ship - no from address set in config") // Create shipment const epShipment = new easypost.Shipment({ to_address: address.easypost_id, from_address: shipping_from.easypost_id, parcel: {length, width, height, weight} }) // Save shipment await epShipment.save() // Purchase shipment (lowest USPS rate) const {tracking_code, id: easypost_id, selected_rate: {rate: price_string}} = await epShipment.buy(epShipment.lowestRate(['USPS'])) const price_cents = Math.floor(parseFloat(price_string) * 100) // Save tracking code and easypost id return await dbUtil.executeFunction({ name: 'set_delivery_easypost', params: [uuid, easypost_id, tracking_code, price_cents], returnType: 'order', single: true }) }