diff --git a/db/index.js b/db/index.js index 93bf2fa..4b11698 100644 --- a/db/index.js +++ b/db/index.js @@ -7,5 +7,6 @@ module.exports = { cart: require('./models/cart'), order: require('./models/order'), address: require('./models/address'), - coupon: require('./models/coupon') + coupon: require('./models/coupon'), + config: require('./models/config') } diff --git a/db/mappings/config.js b/db/mappings/config.js new file mode 100644 index 0000000..c779e12 --- /dev/null +++ b/db/mappings/config.js @@ -0,0 +1,11 @@ +module.exports = [{ + mapId: 'configMap', + idProperty: 'uuid', + properties: [ + 'date_updated', + 'default_tax_percent' + ], + associations: [ + {name: 'modified_by', mapId: 'userMap', columnPrefix: 'user_'}, + ] +}] diff --git a/db/mappings/index.js b/db/mappings/index.js index be1045f..a5a2947 100644 --- a/db/mappings/index.js +++ b/db/mappings/index.js @@ -1,4 +1,5 @@ module.exports = [ + ...require('./config'), ...require('./user'), ...require('./item'), ...require('./order') diff --git a/db/mappings/item.js b/db/mappings/item.js index 43b740d..356a146 100644 --- a/db/mappings/item.js +++ b/db/mappings/item.js @@ -21,8 +21,7 @@ module.exports = [{ 'urlslug', 'price_cents', 'published', - 'number_in_stock', - 'tax_rate' + 'number_in_stock' ], collections: [ {name: 'images', mapId: 'imageMap', columnPrefix: 'image_'} diff --git a/db/models/config.js b/db/models/config.js new file mode 100644 index 0000000..4d439a7 --- /dev/null +++ b/db/models/config.js @@ -0,0 +1,18 @@ +const pg = require('../pg') +const joinjs = require('join-js').default; +const debug = require('debug')('sos:db:config') +const mappings = require('../mappings') + +const config = module.exports = {} + +config.getLatestConfig = async () => { + const {rows} = await pg.query('select * from sos.v_config') + + return joinjs.mapOne(rows, mappings, 'configMap', 'config_') +} + +config.getTaxRate = async () => { + const current = await config.getLatestConfig() + console.log(parseFloat(current.default_tax_percent)) + return parseFloat(current.default_tax_percent) +} diff --git a/db/models/order.js b/db/models/order.js index 0d20e48..e1af367 100644 --- a/db/models/order.js +++ b/db/models/order.js @@ -4,6 +4,8 @@ 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); @@ -45,6 +47,16 @@ order.findAllForSession = async function(session_uuid) { 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 = { @@ -75,24 +87,59 @@ order.addAddress = async function (transaction, address){ if(!price) throw new Error("Unable to estimate price"); - // TODO: Update with real tax rate - const tax = epShipment.to_address.state === 'UT' - ? 200 - : null + // 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, $4)', - values: [transaction.uuid, address.uuid, price, tax] + text: 'select * from sos.add_address_to_order($1, $2, $3)', + values: [transaction.uuid, address.uuid, price] } debug(query) - const {rows} = await pg.query(query) + await pg.query(query) - return joinjs.map(rows, mappings, 'orderMap', 'order_')[0]; + // Update tax + return await order.updateTax(transaction.uuid) } -order.addCoupon = function(transaction, coupon) { +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) @@ -133,7 +180,7 @@ order.addCoupon = function(transaction, coupon) { // Sum the item costs for total discount .reduce((a,c) => (a+c)) - return dbUtil.executeFunction({ + await dbUtil.executeFunction({ name: 'add_coupon_to_transaction', params: [ transaction.uuid, @@ -143,6 +190,9 @@ order.addCoupon = function(transaction, coupon) { returnType: 'order', single: true }) + + // Update tax + return await order.updateTax(transaction.uuid) } order.addPayment = async function(transaction, paymentIntent){ diff --git a/db/sql/1-tables.sql b/db/sql/1-tables.sql index d0dba16..f3f49b8 100644 --- a/db/sql/1-tables.sql +++ b/db/sql/1-tables.sql @@ -46,8 +46,7 @@ create table sos."item" ( item_description text not null, item_urlslug citext unique not null, item_price_cents integer not null, - item_published boolean not null default true, - item_tax_percent_override numeric(8,6) null + item_published boolean not null default true ); create table sos."cart_item" ( @@ -264,5 +263,10 @@ create table sos."item_stockchange_admin" ( ); create table sos."config" ( - config_default_tax_percent numeric(8,6) not null default 7.250 + config_uuid uuid primary key default uuid_generate_v4(), + config_date_updated timestamptz not null default now(), + config_updated_by uuid references sos."user" (user_uuid), + config_default_tax_percent numeric(8,6) not null default 7.250 ); + +insert into sos."config" default values; diff --git a/db/sql/2-views.sql b/db/sql/2-views.sql index 1440f1d..d37532a 100644 --- a/db/sql/2-views.sql +++ b/db/sql/2-views.sql @@ -6,7 +6,6 @@ create or replace view sos.v_item as "item".item_urlslug, "item".item_price_cents, "item".item_published, - coalesce("item".item_tax_percent_override, "config".config_default_tax_percent) as item_tax_rate, "image".image_uuid, "image".image_featured, "image".image_mime_type, @@ -14,7 +13,6 @@ create or replace view sos.v_item as "user".user_email as uploader_email, coalesce(num_added - num_removed, 0) as item_number_in_stock from sos."item" - left join sos."config" on true left join sos."image" on item.item_uuid = image.image_item_uuid left join sos."user" on image.image_uploader_uuid = "user".user_uuid left join @@ -125,3 +123,8 @@ create or replace view sos.v_order as left join sos.v_transaction_paid on "transaction".transaction_uuid = v_transaction_paid.transaction_uuid left join sos.v_payment on "transaction".transaction_uuid = payment_transaction_uuid left join sos.v_cart on cart_uuid = transaction_cart_uuid; + +create or replace view sos.v_config as + select * from sos."config" + left join sos."user" on config_updated_by = user_uuid + where config_date_updated = (select max(config_date_updated) from sos."config") diff --git a/db/sql/3-functions.sql b/db/sql/3-functions.sql index eebf3e9..116eeab 100644 --- a/db/sql/3-functions.sql +++ b/db/sql/3-functions.sql @@ -555,7 +555,7 @@ begin return query select * from sos."address" where address_uuid = _address_uuid; end; $function$; -create or replace function sos.add_address_to_order(_transaction_uuid uuid, _address_uuid uuid, _shipping_price integer, _tax_price integer) +create or replace function sos.add_address_to_order(_transaction_uuid uuid, _address_uuid uuid, _shipping_price integer) returns setof sos.v_order language plpgsql as $function$ @@ -589,13 +589,9 @@ begin where order_uuid = _order_uuid; -- Update transaction with price - update sos."transaction" set ( - transaction_shipping_price, - transaction_tax_price - ) = ( - _shipping_price, - _tax_price - ) where transaction_uuid = _transaction_uuid; + update sos."transaction" set + transaction_shipping_price = _shipping_price + where transaction_uuid = _transaction_uuid; return query select * from sos.v_order where order_uuid = _order_uuid; end; $function$; @@ -671,6 +667,16 @@ begin return query select * from sos.v_order where order_uuid = _order_uuid; end; $function$; +create or replace function sos.find_order_for_transaction(_transaction_uuid uuid) + returns setof sos.v_order + language plpgsql +as $function$ +begin + return query select v_order.* from sos."transaction" + left join sos.v_order on "transaction".transaction_order_uuid = v_order.order_uuid + where "transaction".transaction_uuid = _transaction_uuid; +end; $function$; + create or replace function sos.add_stripe_payment_to_transaction(_transaction_uuid uuid, _payment_value_cents integer, _stripe_intent_id text, _stripe_reciept_email citext) returns setof sos.v_order language plpgsql @@ -806,3 +812,16 @@ begin return query select * from sos.v_item where item_uuid = any(_item_uuids); end; $function$; + +create or replace function sos.add_tax_to_transaction(_transaction_uuid uuid, _tax_price integer) + returns setof sos.v_order + language plpgsql +as $function$ +begin + -- Update transaction + update sos."transaction" set + transaction_tax_price = _tax_price + where transaction_uuid = _transaction_uuid; + + return query select * from sos.find_order_for_transaction(_transaction_uuid); +end; $function$;