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.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 }) 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) throw new Error("Order has no address"); if(_order.address.state !== 'UT'){ debug("Skipping tax for state: " + _order.address.state); return } 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 ], returnType: 'order', single: true }) await dbUtil.executeFunction({ name: 'deduct_stock_for_purchase', params: [transaction.uuid], returnType: 'item', single: false }) return order }