diff --git a/api/orders.js b/api/orders.js index 88b8050..a15e3ee 100644 --- a/api/orders.js +++ b/api/orders.js @@ -5,10 +5,15 @@ const stripe = require('stripe')(process.env.STRIPE_PRIVATE_KEY); const validate = require('./middleware/validators') +router.get('/', async (req, res) => { + const orders = await db.order.findAllForSession(req.session.uuid) + res.json(orders) +}) + router.use(require('./middleware/ensureCart')) router.put('/', async (req, res) => { - const order = await db.order.create(req.cart.uuid); + const order = await db.order.create(req.cart.uuid, req.sessionObj.uuid); res.json(order) }) @@ -66,17 +71,23 @@ router.post('/current/checkout/stripe', async (req, res) => { const itemPriceWithDiscount = item_total_price - (coupon_effective_discount || 0) const shippingPrice = (free_shipping ? 0 : shipping_price) - const line_items = [{ - name: `Cart Total (${numberOfItems} item${numberOfItems > 1 ? 's' : ''})`, - amount: itemPriceWithDiscount, - currency: 'usd', - quantity: 1 - }, { - name: 'Shipping', - amount: shippingPrice, - currency: 'usd', - quantity: 1 - }] + const line_items = [] + + if(itemPriceWithDiscount > 0) + line_items.push({ + name: `Cart Total (${numberOfItems} item${numberOfItems > 1 ? 's' : ''})`, + amount: itemPriceWithDiscount, + currency: 'usd', + quantity: 1 + }) + + if(shippingPrice > 0) + line_items.push({ + name: 'Shipping', + amount: shippingPrice, + currency: 'usd', + quantity: 1 + }) if(tax_price > 0) line_items.push({ @@ -86,12 +97,16 @@ router.post('/current/checkout/stripe', async (req, res) => { quantity: 1 }) + // TODO: We need to handle this case, mark it paid or something? + if(line_items.length < 1) + res.json(null) + const session = await stripe.checkout.sessions.create({ client_reference_id: currentTransaction.uuid, customer_email: req.user ? req.user.email : undefined, payment_method_types: ['card'], line_items, - success_url: `${process.env.EXTERNAL_URL}/store/checkout/complete?session_id={CHECKOUT_SESSION_ID}`, + success_url: `${process.env.EXTERNAL_URL}/store/checkout/verify?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${process.env.EXTERNAL_URL}/cancel`, }); @@ -118,5 +133,7 @@ router.post('/current/checkout/verify', parseJSON, async (req, res) => { await db.session.clearCart(req.sessionObj.uuid) } + // TODO: Save stockchange record for purchase + res.json({status: payment.status, order}) }) diff --git a/db/mappings/item.js b/db/mappings/item.js index dbf3f5b..356a146 100644 --- a/db/mappings/item.js +++ b/db/mappings/item.js @@ -9,7 +9,8 @@ module.exports = [{ 'date_uploaded' ], associations: [ - {name: 'uploader', mapId: 'userMap', columnPrefix: 'user_'} + // TODO: Uploader should not be included in non-admin API responses + {name: 'uploader', mapId: 'userMap', columnPrefix: 'uploader_'} ] },{ mapId: 'itemMap', diff --git a/db/mappings/order.js b/db/mappings/order.js index a788249..5b660b2 100644 --- a/db/mappings/order.js +++ b/db/mappings/order.js @@ -26,7 +26,8 @@ module.exports = [{ ], associations: [ {name: 'cart', mapId: 'cartMap', columnPrefix: 'cart_'}, - {name: 'coupon', mapId: 'couponMap', columnPrefix: 'coupon_'} + {name: 'coupon', mapId: 'couponMap', columnPrefix: 'coupon_'}, + {name: 'payment', mapId: 'paymentMap', columnPrefix: 'payment_'} ] },{ mapId: 'addressMap', @@ -56,4 +57,21 @@ module.exports = [{ 'per_sock_discount_cents', 'number_of_socks_free' ] +},{ + mapId: 'paymentMap', + idProperty: 'uuid', + properties: [ + 'type', + 'time', + 'value_cents' + ], + associations: [ + {name: 'stripe', mapId: 'paymentStripeMap', columnPrefix: 'stripe_'} + ] +},{ + mapId: 'paymentStripeMap', + idProperty: 'payment_intent_id', + properties: [ + 'reciept_email' + ] }] diff --git a/db/models/order.js b/db/models/order.js index 11f0fb1..1030a48 100644 --- a/db/models/order.js +++ b/db/models/order.js @@ -9,10 +9,10 @@ const easypost = new (require('@easypost/api'))(process.env.EASYPOST_API_KEY); const order = module.exports = {} -order.create = async function(cart_uuid){ +order.create = async function(cart_uuid, session_uuid){ const query = { - text: 'select * from sos.create_order($1)', - values: [cart_uuid] + text: 'select * from sos.create_order($1, $2)', + values: [cart_uuid, session_uuid] } debug(query); @@ -33,6 +33,18 @@ order.findForCart = async function(cart_uuid) { 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.addAddress = async function (transaction, address){ // Get parcel size const parcel = { diff --git a/db/sql/1-tables.sql b/db/sql/1-tables.sql index be452be..d5d7224 100644 --- a/db/sql/1-tables.sql +++ b/db/sql/1-tables.sql @@ -171,6 +171,7 @@ create table sos."coupon" ( create table sos."transaction" ( transaction_uuid uuid primary key default uuid_generate_v4(), transaction_order_uuid uuid references sos."order" (order_uuid), + transaction_session_uuid uuid not null references sos."session" (session_uuid), transaction_cart_uuid uuid references sos."cart" (cart_uuid), transaction_coupon_uuid uuid references sos."coupon" (coupon_uuid), transaction_start_time timestamptz not null default now(), diff --git a/db/sql/2-views.sql b/db/sql/2-views.sql index 3aca3d2..d116bf6 100644 --- a/db/sql/2-views.sql +++ b/db/sql/2-views.sql @@ -5,7 +5,7 @@ create or replace view sos.v_item as "image".image_featured, "image".image_mime_type, "image".image_date_uploaded, - "user".user_email, + "user".user_email as uploader_email, coalesce(num_added - num_removed, 0) as item_number_in_stock from sos."item" left join sos."image" on item.item_uuid = image.image_item_uuid diff --git a/db/sql/3-functions.sql b/db/sql/3-functions.sql index 58a221b..3320237 100644 --- a/db/sql/3-functions.sql +++ b/db/sql/3-functions.sql @@ -402,13 +402,14 @@ begin return query select * from sos.v_item where item_uuid = _item_uuid; end; $function$; -create or replace function sos.create_order(_cart_uuid uuid) +create or replace function sos.create_order(_cart_uuid uuid, _session_uuid uuid) returns setof sos.v_order language plpgsql as $function$ declare _completed_transactions integer; _order_uuid uuid; + _existing_transaction uuid; _cart_price integer; begin -- Check for completed transactions @@ -422,11 +423,18 @@ begin end if; -- Check for existing transaction for this cart? - select transaction_order_uuid into _order_uuid + select transaction_order_uuid, transaction_uuid into _order_uuid, _existing_transaction from sos."transaction" where transaction_cart_uuid = _cart_uuid and transaction_payment_state = 'started'; + -- Update existing transaction to current session + if _existing_transaction is not null then + update sos."transaction" set + transaction_session_uuid = _session_uuid + where transaction_uuid = _existing_transaction; + end if; + -- If no existing order, create an order and a transaction if _order_uuid is null then -- Create order @@ -442,11 +450,13 @@ begin insert into sos."transaction" ( transaction_order_uuid, transaction_cart_uuid, - transaction_item_total_price + transaction_item_total_price, + transaction_session_uuid ) values ( _order_uuid, _cart_uuid, - _cart_price + _cart_price, + _session_uuid ); end if; @@ -495,6 +505,22 @@ begin return query select * from sos.v_order where order_uuid = _order_uuid; end; $function$; +create or replace function sos.find_orders_for_session(_session_uuid uuid) + returns setof sos.v_order + language plpgsql +as $function$ +begin + return query + select "order".* from sos."transaction" + left join sos.v_order "order" on "order".order_uuid = "transaction".transaction_order_uuid + where "transaction".transaction_session_uuid = _session_uuid + and ( + "transaction".transaction_payment_state = 'started' + or + "transaction".transaction_payment_state = 'completed' + ); +end; $function$; + create or replace function sos.create_address(_name text, _company text, _street1 text, _street2 text, _city text, _state text, _zip text, _country text, _phone text, _easypost_id text) returns setof sos."address" language plpgsql diff --git a/pages/store/checkout/complete.js b/pages/store/checkout/complete.js index df1eebb..333b1c0 100644 --- a/pages/store/checkout/complete.js +++ b/pages/store/checkout/complete.js @@ -1,16 +1,37 @@ -import {useEffect} from 'react' -import Router from 'next/router' - +import {DateTime} from 'luxon' CheckoutComplete.getInitialProps = async function({ctx: {query: {session_id}, axios}}){ - const {data: {status, order}} = await axios.post('/api/orders/current/checkout/verify', {session_id}) - return {status, order} + const {data: orders} = await axios.get('/api/orders') + + const mostRecentOrder = orders.sort(sortOrders)[0] + + return {order: mostRecentOrder} } -export default function CheckoutComplete({status, order}){ +export default function CheckoutComplete({order}){ return (
{JSON.stringify(order, null, 2)}) } + +function parsePaymentTime({payment}){ + if(typeof payment.time === 'string') + payment.time = DateTime.fromISO(payment.time) + return payment.time +} + +function sortTransactions(a,b){ + const timeA = parsePaymentTime(a) + const timeB = parsePaymentTime(b) + + return timeB.diff(timeA).as('seconds') +} + +function sortOrders(a,b){ + const timePaidA = parsePaymentTime(a.transactions.sort(sortTransactions)[0]) + const timePaidB = parsePaymentTime(b.transactions.sort(sortTransactions)[0]) + + return timePaidB.diff(timePaidA).as('seconds') +} diff --git a/pages/store/checkout/verify.js b/pages/store/checkout/verify.js new file mode 100644 index 0000000..1d08a82 --- /dev/null +++ b/pages/store/checkout/verify.js @@ -0,0 +1,39 @@ +import {useState, useEffect} from 'react' +import axios from 'axios' +import Router from 'next/router' + + +CheckoutComplete.getInitialProps = async function({ctx: {query: {session_id}}}){ + return {session_id} +} + +export default function CheckoutComplete({session_id}){ + const [loading, setLoading] = useState(true) + + useEffect(()=>{ + (async ()=>{ + const {data: {status}} = await axios.post('/api/orders/current/checkout/verify', {session_id}) + + if(status === "succeeded") + Router.push('/store/checkout/complete') + else + setLoading(false) + + })() + }, []) + + if(loading) + return ( + <> +
Verifying your payment . . .
+ > + ) + + return ( + <> +There was a problem with your payment.
+ > + ) +}