|
|
|
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 user = require('./user')
|
|
|
|
|
|
|
|
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.getUser = async function(order_uuid){
|
|
|
|
const query = {
|
|
|
|
text: 'select order_user_uuid from sos.order where order_uuid = $1',
|
|
|
|
values: [order_uuid]
|
|
|
|
}
|
|
|
|
|
|
|
|
debug(query)
|
|
|
|
const {rows} = await pg.query(query)
|
|
|
|
|
|
|
|
const user_uuid = rows[0]?.order_user_uuid
|
|
|
|
return user.findById(user_uuid)
|
|
|
|
}
|
|
|
|
|
|
|
|
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, items ) => {
|
|
|
|
// 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")
|
|
|
|
|
|
|
|
if(!shipping_from?.phone)
|
|
|
|
throw new Error("Cannot ship - Selected from address has no phone")
|
|
|
|
|
|
|
|
let purchasedShipment;
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Create shipment
|
|
|
|
const epShipment = new easypost.Shipment({
|
|
|
|
to_address: address.easypost_id,
|
|
|
|
from_address: shipping_from.easypost_id,
|
|
|
|
parcel: {length, width, height, weight}
|
|
|
|
})
|
|
|
|
|
|
|
|
if(address.country !== 'US') {
|
|
|
|
const customsInfo = new easypost.CustomsInfo({
|
|
|
|
eel_pfc: 'NOEEI 30.37(a)',
|
|
|
|
contents_type: 'merchandise',
|
|
|
|
contents_explanation: 'Goods purchased from societyofsocks.us',
|
|
|
|
customs_certify: true,
|
|
|
|
customs_signer: 'Kirk Hamilton',
|
|
|
|
restriction_type: 'none',
|
|
|
|
customs_items: items.map(({item, count}) => ({
|
|
|
|
description: item.customs_description,
|
|
|
|
quantity: count,
|
|
|
|
value: (item.price_cents * count / 100).toFixed(2),
|
|
|
|
weight: item.weight_oz * count,
|
|
|
|
hs_tariff_number: item.hs_tariff_number,
|
|
|
|
origin_countr: item.customs_origin_country
|
|
|
|
}))
|
|
|
|
})
|
|
|
|
|
|
|
|
await customsInfo.save()
|
|
|
|
|
|
|
|
epShipment.customs_info = customsInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save shipment
|
|
|
|
await epShipment.save()
|
|
|
|
|
|
|
|
// Purchase shipment (lowest USPS rate)
|
|
|
|
purchasedShipment = await epShipment.buy(epShipment.lowestRate(['USPS']))
|
|
|
|
} catch (err) {
|
|
|
|
console.log(err)
|
|
|
|
throw new Error("Cannot purchase shipment - see log for details")
|
|
|
|
}
|
|
|
|
|
|
|
|
const {tracking_code, id: easypost_id, selected_rate: {rate: price_string}} = purchasedShipment
|
|
|
|
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
|
|
|
|
})
|
|
|
|
}
|