You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

245 lines
6.7 KiB
JavaScript

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
})
5 years ago
// 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)
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
}