Merge branch 'pre-order' into 'master'

Preorder

See merge request tempest_dawn/sos-nextjs!1
main
Ashelyn Dawn 3 years ago
commit 7c6937df3c

@ -90,3 +90,19 @@ router.post('/:uuid/unpublish', ensureAdmin, async (req, res) => {
const item = await db.item.unpublish(req.params.uuid)
res.json(item)
})
router.get('/:uuid/preorder/count', ensureAdmin, async (req, res) => {
const count = await db.item.getPreorderCount(req.params.uuid);
res.json(count)
})
router.post('/:uuid/preorder', ensureAdmin, parseJSON, async (req, res) => {
const item = await db.item.setPreorder(
req.params.uuid,
req.body.availability_date,
req.body.preorder_maximum
)
res.json(item);
})

@ -18,6 +18,14 @@ export default function Card({item}) {
setCart(newCart)
}
const canPurchase = item.number_in_stock > 0 || item.preorder_availability_date !== undefined
const availabilityText =
item.number_in_stock > 0
? `${item.number_in_stock} in stock`
: item.preorder_availability_date && (-1 * item.number_in_stock < item.preorder_maximum)
? 'Available for pre-order'
: 'Currently out of stock'
return (
<div className={styles.card}>
<h3><Link href={`/store/item/${item.urlslug}`}><a>{item.name}</a></Link></h3>
@ -32,15 +40,11 @@ export default function Card({item}) {
<div className={styles['card-text']}>{item.description}</div>
<ul className={styles['card-details'] + (item.number_in_stock > 0 ? '' : ' ' + styles['out-of-stock'])}>
<li className={styles['number-in-stock']}>
{
item.number_in_stock > 0
? `${item.number_in_stock} in stock`
: 'Currently out of stock'
}
{availabilityText}
</li>
<li><Link href={`/store/item/${item.urlslug}`}><a>Details</a></Link></li>
{
item.number_in_stock > 0 && (
canPurchase && (
<li>
<button className="buttonLink" onClick={addToCart}>Add to Cart</button>
</li>

@ -86,7 +86,7 @@ export const FormController = function FormController({children, className, url,
// Update initial state
React.Children.forEach(children, child => {
if(!child.props.name) return;
if(!child?.props?.name) return;
initialState.fields[child.props.name] = {
name: child.props.name,
@ -149,6 +149,8 @@ export const FormController = function FormController({children, className, url,
// Map children
const _children = React.Children.map(children, child => {
if(!child) return null;
if(child.type === Button && child.props.type?.toLowerCase() === 'submit')
return React.cloneElement(child, {
// Allow enabled prop to disable, but not solely enable

@ -84,6 +84,19 @@ function formatTime(time){
}
function ShippingStatus({order, totalShipping, delivery, isAdmin}) {
const preorderDates = order.transactions.filter(t => t.has_preorder && !t.preorder_ready_to_ship).map(t => DateTime.fromISO(t.preorder_fulfill_date))
preorderDates.sort((b, a) => a < b ? -1 : a > b ? 1 : 0)
const latestPreorder = preorderDates[0]?.toLocaleString(DateTime.DATE_FULL)
if(!delivery && latestPreorder)
return (
<>
{isAdmin && (<p>{formatMoney(totalShipping)} charged to customer</p>)}
<p>Preorder - awaiting stock.</p>
<p>Expected to be in stock {latestPreorder}</p>
</>
)
if(!delivery)
return (
<>

@ -22,6 +22,8 @@ module.exports = [{
'price_cents',
'published',
'number_in_stock',
'preorder_availability_date',
'preorder_maximum',
'hs_tariff_number',
'customs_description',
'customs_origin_country',

@ -23,7 +23,10 @@ module.exports = [{
'item_total_price',
'coupon_effective_discount',
'shipping_price',
'tax_price'
'tax_price',
'has_preorder',
'preorder_fulfill_date',
'preorder_ready_to_ship'
],
associations: [
{name: 'cart', mapId: 'cartMap', columnPrefix: 'cart_'},

@ -52,7 +52,7 @@ item.findBySlug = async (item_slug) => {
return joinjs.map(rows, mappings, 'itemMap', 'item_')[0];
}
item.create = async (name, urlslug, description, price_cents, published, hs_tariff_number, customs_description, origin_country, weight) =>
item.create = async (name, urlslug, description, price_cents, published, hs_tariff_number, customs_description, origin_country, weight) =>
dbUtil.executeFunction({
name: 'create_item',
params: [
@ -89,6 +89,21 @@ item.update = async (uuid, name, urlslug, description, price_cents, published) =
return joinjs.map(rows, mappings, 'itemMap', 'item_')[0]
}
item.setPreorder = (uuid, estimated_delivery, max_preorders) =>
dbUtil.executeFunction({
name: 'set_preorder',
params: [uuid, estimated_delivery, max_preorders],
returnType: 'item',
single: true
})
item.getPreorderCount = (uuid) =>
dbUtil.executeFunction({
name: 'get_number_of_preorder_reservations',
params: [uuid],
returnType: 'raw'
})
item.addImage = async (item_uuid, image_buffer, uploader_uuid) => {
// Default param chain: output as png
const source = sharp(image_buffer).png()

@ -1,7 +1,7 @@
create type sos."delivery_type_enum" as enum ('hand_shipped', 'easypost', 'hand_delivered');
create type sos."transaction_state_enum" as enum ('started', 'completed', 'cancelled', 'expired');
create type sos."payment_type_enum" as enum ('ks_reward', 'stripe', 'paypal', 'account_credit', 'admin_granted');
create type sos."stockchange_type_enum" as enum ('purchase', 'shipment', 'admin');
create type sos."stockchange_type_enum" as enum ('purchase', 'shipment', 'admin', 'preorder');
create type sos."stock_change_dir_enum" as enum ('added', 'subtracted');
create type sos."email_link_type_enum" as enum ('email_confirm', 'email_change', 'password_reset');
@ -53,7 +53,14 @@ create table sos."item" (
item_customs_description text not null,
item_customs_origin_country text not null,
item_weight_oz integer not null,
item_published boolean not null default true
item_published boolean not null default true,
item_preorder_availability_date timestamptz,
item_preorder_maximum integer check (item_preorder_maximum is null or item_preorder_maximum >= 0),
constraint item_cannot_have_preorder_date_without_max check (
( item_preorder_availability_date is not null and item_preorder_maximum is not null )
or ( item_preorder_availability_date is null and item_preorder_maximum is null )
)
);
create table sos."cart_item" (
@ -267,6 +274,16 @@ create table sos."item_stockchange_purchase" (
stockchange_transaction_uuid uuid references sos."transaction" (transaction_uuid)
);
create table sos."item_stockchange_preorder" (
stockchange_uuid uuid primary key default uuid_generate_v4(),
stockchange_type sos.stockchange_type_enum check (stockchange_type = 'preorder'),
foreign key (stockchange_uuid, stockchange_type) references sos."item_stockchange" (stockchange_uuid, stockchange_type),
stockchange_transaction_uuid uuid references sos."transaction" (transaction_uuid),
stockchange_preorder_estimated_fulfill_date timestamptz not null,
stockchange_preorder_ready_to_ship boolean not null default false
);
create table sos."item_stockchange_shipment" (
stockchange_uuid uuid primary key default uuid_generate_v4(),
stockchange_type sos.stockchange_type_enum check (stockchange_type = 'shipment'),

@ -132,6 +132,9 @@ create or replace view sos.v_order as
+ case when (coupon_free_shipping != true) then transaction_shipping_price else 0 end
+ coalesce(transaction_tax_price, 0)
) as transaction_computed_price,
(case when stockchange_type = 'preorder' then true else false end) as transaction_has_preorder,
stockchange_preorder_estimated_fulfill_date as transaction_preorder_fulfill_date,
stockchange_preorder_ready_to_ship as transaction_preorder_ready_to_ship,
"coupon".*,
"address".*,
v_transaction_paid.transaction_amount_paid_cents,
@ -145,7 +148,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_delivery on "order".order_delivery_uuid = delivery_uuid
left join sos.v_cart on cart_uuid = transaction_cart_uuid;
left join sos.v_cart on cart_uuid = transaction_cart_uuid
left join sos.item_stockchange_preorder on stockchange_transaction_uuid = transaction.transaction_uuid;
create or replace view sos.v_config as
select * from sos."config"
@ -162,6 +166,7 @@ create or replace view sos.v_stockchange as
from sos."item_stockchange"
left join sos."item_stockchange_shipment" on item_stockchange.stockchange_uuid = item_stockchange_shipment.stockchange_uuid
left join sos."item_stockchange_purchase" on item_stockchange.stockchange_uuid = item_stockchange_purchase.stockchange_uuid
left join sos."item_stockchange_preorder" on item_stockchange.stockchange_uuid = item_stockchange_preorder.stockchange_uuid
left join sos."item_stockchange_admin" on item_stockchange.stockchange_uuid = item_stockchange_admin.stockchange_uuid;
create or replace view sos.v_shipment as

@ -986,7 +986,9 @@ declare
_item_uuids uuid[];
_cart_uuid uuid;
_item_uuid uuid;
_item_count integer;
_cart_item_count integer;
_available_item_count integer;
_item_availability_date timestamptz;
_stockchange_uuid uuid;
begin
-- Get cart uuid (and check completed)
@ -1009,31 +1011,65 @@ begin
-- Make stock-change record for each item
foreach _item_uuid in array _item_uuids loop
select cart_item_count into _item_count from sos."cart_item"
-- Get cart and item info
select cart_item_count into _cart_item_count from sos."cart_item"
where cart_item_cart_uuid = _cart_uuid
and cart_item_item_uuid = _item_uuid;
insert into sos."item_stockchange" (
stockchange_type,
stockchange_item_uuid,
stockchange_change,
stockchange_direction
) values (
'purchase',
_item_uuid,
_item_count,
'subtracted'
) returning stockchange_uuid into _stockchange_uuid;
insert into sos."item_stockchange_purchase" (
stockchange_uuid,
stockchange_type,
stockchange_transaction_uuid
) values (
_stockchange_uuid,
'purchase',
_transaction_uuid
);
select
item_number_in_stock, item_preorder_availability_date
into _available_item_count, _item_availability_date
from sos.v_item
where item_uuid = _item_uuid;
-- Pre-order stockchange
if _cart_item_count > _available_item_count and _item_availability_date is not null then
insert into sos."item_stockchange" (
stockchange_type,
stockchange_item_uuid,
stockchange_change,
stockchange_direction
) values (
'preorder',
_item_uuid,
_cart_item_count,
'subtracted'
) returning stockchange_uuid into _stockchange_uuid;
insert into sos."item_stockchange_preorder" (
stockchange_uuid,
stockchange_type,
stockchange_transaction_uuid,
stockchange_preorder_estimated_fulfill_date
) values (
_stockchange_uuid,
'preorder',
_transaction_uuid,
_item_availability_date
);
else -- Non-preorder stockchange
insert into sos."item_stockchange" (
stockchange_type,
stockchange_item_uuid,
stockchange_change,
stockchange_direction
) values (
'purchase',
_item_uuid,
_cart_item_count,
'subtracted'
) returning stockchange_uuid into _stockchange_uuid;
insert into sos."item_stockchange_purchase" (
stockchange_uuid,
stockchange_type,
stockchange_transaction_uuid
) values (
_stockchange_uuid,
'purchase',
_transaction_uuid
);
end if;
end loop;
return query select * from sos.v_item where item_uuid = any(_item_uuids);
@ -1095,11 +1131,87 @@ begin
'shipment',
_shipment_uuid
);
perform sos.mark_preorders_shippable(_item_uuids[i], _counts[i]);
end loop;
return query select * from sos.v_shipment where shipment_uuid = _shipment_uuid;
end; $function$;
create or replace function sos.mark_preorders_shippable(_item_uuid uuid, _count integer)
returns void
language plpgsql
as $function$
declare
_preorder_uuids uuid[];
_num_marked integer := 0;
_current_num integer;
_current_direction sos.stock_change_dir_enum;
_remaining_preorders integer;
begin
-- Get unfulfilled pre-order stockchanges for this item, ordered by transaction completion time
select array_agg(item_stockchange.stockchange_uuid order by transaction_completion_time) into _preorder_uuids
from sos.item_stockchange
left join sos.item_stockchange_preorder on item_stockchange.stockchange_uuid = item_stockchange_preorder.stockchange_uuid
left join sos.transaction on stockchange_transaction_uuid = transaction_uuid
where stockchange_item_uuid = _item_uuid
and item_stockchange.stockchange_type = 'preorder'
and stockchange_preorder_ready_to_ship = false
group by stockchange_item_uuid;
-- For all stockchanges
if _preorder_uuids is not null then
for i in array_lower(_preorder_uuids, 1) .. array_upper(_preorder_uuids, 1) loop
select
stockchange_direction,
stockchange_change
into
_current_direction,
_current_num
from sos.item_stockchange
where stockchange_uuid = _preorder_uuids[i];
-- Sanity check: Only fulfill ones that deducted stock
if _current_direction != 'subtracted' then
continue;
end if;
-- Don't fulfill more than we have
if _num_marked + _current_num > _count then
exit;
end if;
update sos.item_stockchange_preorder set
stockchange_preorder_ready_to_ship = true
where stockchange_uuid = _preorder_uuids[i];
_num_marked := _num_marked + _current_num;
end loop;
end if;
-- Count remaining pre-orders
select count(item_stockchange.stockchange_uuid) into _remaining_preorders
from sos.item_stockchange
left join sos.item_stockchange_preorder on item_stockchange.stockchange_uuid = item_stockchange_preorder.stockchange_uuid
where stockchange_item_uuid = _item_uuid
and item_stockchange.stockchange_type = 'preorder'
and stockchange_preorder_ready_to_ship = false
group by stockchange_item_uuid;
-- If we no longer have pre-orders clear item pre-order records
if _remaining_preorders < 1 then
update sos.item
set (
item_preorder_availability_date,
item_preorder_maximum
) = (
null,
null
)
where item_uuid = _item_uuid;
end if;
end; $function$;
create or replace function sos.set_delivery_tracking(_order_uuid uuid, _tracking_number text, _date_shipped timestamptz, _price_cents integer)
returns setof sos.v_order
language plpgsql
@ -1336,4 +1448,44 @@ begin
);
return query select * from sos.v_config;
end; $function$;
create or replace function sos.get_number_of_preorder_reservations(_item_uuid uuid)
returns integer
language plpgsql
as $function$
begin
return (
select
count(*)
from sos.item_stockchange_preorder
left join sos.item_stockchange
on item_stockchange.stockchange_uuid = item_stockchange_preorder.stockchange_uuid
where stockchange_preorder_ready_to_ship = false
and stockchange_item_uuid = _item_uuid
);
end; $function$;
create or replace function sos.set_preorder(_item_uuid uuid, _estimated_delivery timestamptz, _max_preorders integer)
returns setof sos.v_item
language plpgsql
as $function$
declare
_num_reserved integer := sos.get_number_of_preorder_reservations(_item_uuid);
begin
if _num_reserved > _max_preorders then
raise 'Cannot lower preorder limit past the number of reservations';
end if;
update sos.item
set (
item_preorder_availability_date,
item_preorder_maximum
) = (
_estimated_delivery,
_max_preorders
)
where item_uuid = _item_uuid;
return query select * from sos.v_item where item_uuid = _item_uuid;
end; $function$;

@ -12,6 +12,9 @@ util.executeQuery = async function({query, returnType, tablePrefix, single = fal
const {rows} = await pg.query(query)
if(returnType === 'raw')
return rows
const mappedObjs = joinjs.map(rows, mappings, returnType + 'Map', tablePrefix || (returnType + '_'))
if(single)
@ -27,5 +30,10 @@ util.executeFunction = async function({name, params, returnType, single, tablePr
values: params
}
return util.executeQuery({query, returnType, single, tablePrefix})
const result = await util.executeQuery({query, returnType, single, tablePrefix})
if(returnType === 'raw' && result[0][name] !== undefined)
return result[0][name]
return result
}

@ -61,6 +61,7 @@ export default function AccountPage({orders}) {
{name: 'Item Price', extractor: getItemPrice},
{name: 'Shipping', extractor: getShippingEstimate},
{name: 'Total', extractor: getAmountPaid},
{name: 'Status', extractor: getStatus},
{name: '', extractor: order =>
<button className="buttonLink" onClick={() => Router.push(`/account/orders/${order.number}`)}>Details</button>
}
@ -104,6 +105,21 @@ function getAmountPaid(order){
.reduce((a,b)=>(a+b)))
}
function getStatus(order) {
if (order.delivery && order.delivery.type == "hand_delivered")
return 'Delivered in person'
if (order.delivery)
return `Shipped ${DateTime.fromISO(order.delivery.date_shipped).toLocaleString(DateTime.DATE_SHORT)}`
const hasPreorder = order.transactions.some(t => t.has_preorder && !t.preorder_ready_to_ship)
if (!hasPreorder)
return 'Awaiting Shipment'
return 'Preorder'
}
function parsePaymentTime({payments}){
for(const payment of payments) {
if(typeof payment.time === 'string')

@ -27,6 +27,11 @@ export default function EditItem({item}) {
return (
<>
<ActionBar title={`Editing "${item.name}"`}>
{
item.preorder_availability_date
? <RMWCButton outlined onClick={()=>router.push(`/admin/items/${item.urlslug}/preorder`)}>Edit Pre-order</RMWCButton>
: <RMWCButton outlined onClick={()=>router.push(`/admin/items/${item.urlslug}/preorder`)}>Set Pre-order</RMWCButton>
}
<RMWCButton outlined onClick={()=>router.push(`/admin/items/${item.urlslug}/images`)}>Edit Images ({item.images.length})</RMWCButton>
</ActionBar>
<FormController url={`/api/items/${item.uuid}`} afterSubmit={afterUpdate}>

@ -0,0 +1,50 @@
import React from 'react'
import router from 'next/router'
import axios from 'axios'
import ActionBar from '~/components/admin/actionBar'
import {Button as RMWCButton} from '@rmwc/button'
import {FormController, IntegerInput, Button, DateInput} from '~/components/form'
EditItem.getInitialProps = async ({ctx: {axios, query: {slug}}}) => {
const {data: item} = await axios.get(`/api/items/by-slug/${slug}`)
const {data: numPreorders} = await axios.get(`/api/items/${item.uuid}/preorder/count`)
return {item, numPreorders}
}
export default function EditItem({item, numPreorders}) {
const minimum = Math.max(numPreorders, 1)
const initial = Math.max(minimum, item.preorder_maximum);
const afterUpdate = () => {
router.push(`/admin/items/${item.urlslug}`)
}
const deletePreorder = async () => {
await axios.post(`/api/items/${item.uuid}/preorder`, {availability_date: null, preorder_maximum: null})
router.push(`/admin/items/${item.urlslug}`)
}
return (
<>
{
item.preorder_availability_date
? <ActionBar title={`Updating pre-order for "${item.name}"`}>
{numPreorders < 1 &&
<RMWCButton outlined onClick={deletePreorder}>Remove pre-order</RMWCButton>
}
</ActionBar>
: <ActionBar title={`Adding pre-order for "${item.name}"`}/>
}
<FormController url={`/api/items/${item.uuid}/preorder`} afterSubmit={afterUpdate}>
<DateInput label="Estimated Delivery" initialValue={item.preorder_availability_date} minDate={new Date()} name="availability_date" />
<IntegerInput label="Max pre-orders" initialValue={initial.toFixed(0)} name="preorder_maximum" minimum={minimum.toFixed(0)} validate={i => i > 0} />
{item.preorder_availability_date &&
<p>There are currently {numPreorders} pre-orders for this item</p>
}
<Button type="submit">Create</Button>
</FormController>
</>
)
}

@ -10,7 +10,8 @@ Orders.getInitialProps = async ({ctx}) => {
}
export default function Orders({orders}){
const unshippedOrders = orders.filter(order => !order.delivery)
const unshippedPreOrders = orders.filter(order => !order.delivery && order.transactions.some(t => t.has_preorder && !t.preorder_ready_to_ship))
const unshippedAndShippableOrders = orders.filter(order => !order.delivery && !order.transactions.some(t => t.has_preorder && !t.preorder_ready_to_ship))
const shippedOrders = orders.filter(order => order.delivery).reverse()
return (
@ -18,7 +19,7 @@ export default function Orders({orders}){
<ActionBar title="Orders" actions={[
{label: 'Add Manual Order', url: `/admin/orders/new`}
]}/>
<h4>Unsent:</h4>
<h4>Unsent (sendable):</h4>
<Table
columns={[
{name: 'Purchased', extractor: getPurchaseTime},
@ -30,7 +31,23 @@ export default function Orders({orders}){
<button className="buttonLink" onClick={() => Router.push(`/admin/orders/${row.id}`)}>Details</button>
}
]}
rows={unshippedOrders.map(order => ({id: order.uuid, ...order}))}
rows={unshippedAndShippableOrders.map(order => ({id: order.uuid, ...order}))}
/>
<h4>Unsent (pre-orders):</h4>
<Table
columns={[
{name: 'Purchased', extractor: getPurchaseTime},
{name: 'Items', extractor: getNumberItems},
{name: 'Price', extractor: getItemPrice},
{name: 'Shipping', extractor: getShippingEstimate},
{name: 'Amount Paid', extractor: getAmountPaid},
{name: 'Estimated availability', extractor: getPreorderEstimate},
{name: '', extractor: row =>
<button className="buttonLink" onClick={() => Router.push(`/admin/orders/${row.id}`)}>Details</button>
}
]}
rows={unshippedPreOrders.map(order => ({id: order.uuid, ...order}))}
/>
<h4>Sent:</h4>
@ -88,6 +105,12 @@ function getAmountPaid(order){
.reduce((a,b)=>(a+b), 0))
}
function getPreorderEstimate(order) {
const preorderDates = order.transactions.filter(t => t.has_preorder).map(t => DateTime.fromISO(t.preorder_fulfill_date))
preorderDates.sort((b, a) => a < b ? -1 : a > b ? 1 : 0)
return preorderDates[0]?.toLocaleString(DateTime.DATE_FULL)
}
function parsePaymentTime({payments}){
for(const payment of payments) {
if(typeof payment.time === 'string')

@ -3,17 +3,30 @@ import Router from 'next/router'
import Link from 'next/link'
import axios from 'axios'
import Head from 'next/head'
import {DateTime} from 'luxon'
import useCart, {useSetCart} from '~/hooks/useCart'
import {FormController, Input, Button} from '~/components/form'
import {FormController, Button} from '~/components/form'
import Table from '~/components/table'
export default function Cart(){
const cart = useCart()
const setCart = useSetCart()
const numItems = (cart?.items) ? cart.items.length : 0
const allInStock = !cart?.items?.some(item => !item.item.number_in_stock || item.item.number_in_stock < 1)
const allHaveEnough = !cart?.items?.some(item => item.count > item.item.number_in_stock)
cart?.items?.forEach(({item, count}) => {
const stock = item.stock = {}
stock.is_preorder = item.preorder_availability_date && count > item.number_in_stock
stock.available = stock.is_preorder ? item.preorder_maximum - -Math.min(0, item.number_in_stock) : item.number_in_stock
stock.has_enough = count <= stock.available
})
const allInStock = !cart?.items?.some(item => !(item.item.number_in_stock > 0 || item.item.preorder_availability_date) )
const allHaveEnough = !cart?.items?.some(item => !item.item.stock.has_enough)
const preorderItems = cart?.items?.filter(({item, count}) => count > item.number_in_stock && item.preorder_availability_date).map(({item}) => DateTime.fromISO(item.preorder_availability_date)) || []
preorderItems.sort((b, a) => a < b ? -1 : a > b ? 1 : 0)
const latestPreorder = preorderItems[0]?.toLocaleString(DateTime.DATE_FULL)
const handleRemove = id => async ev => {
if(ev) ev.preventDefault()
@ -42,8 +55,23 @@ export default function Cart(){
{name: 'Item', extractor: row => (
<>
<Link href={`/store/item/${row.item.urlslug}`}><a>{row.item.name}</a></Link>
{(!row.item.number_in_stock || row.item.number_in_stock < 1) && <strong style={{marginLeft: '6px'}}>Out of stock</strong>}
{(row.item.number_in_stock > 0 && row.count > row.item.number_in_stock) && <strong style={{marginLeft: '6px'}}>Not enough in stock</strong>}
{(() => {
const {stock} = row.item;
if(stock.is_preorder && !stock.has_enough)
return <strong style={{marginLeft: '6px'}}>Not enough expected stock</strong>
if(stock.is_preorder && stock.has_enough)
return <strong style={{marginLeft: '6px'}}>(Pre-order: available {DateTime.fromISO(row.item.preorder_availability_date).toLocaleString(DateTime.DATE_SHORT)})</strong>
if(!row.item.number_in_stock || row.item.number_in_stock < 1)
return <strong style={{marginLeft: '6px'}}>Out of stock</strong>
if (!stock.has_enough)
return <strong style={{marginLeft: '6px'}}>Not enough in stock</strong>
return null
})()}
</>
)},
{name: 'Quantity in Cart', extractor: row => row.count},
@ -92,6 +120,13 @@ export default function Cart(){
return <Button type="submit">Proceed to Checkout</Button>
})()}
{latestPreorder && allHaveEnough && (
<p className="warning">
<strong>Note: </strong>
Your cart contains one or more pre-order items. If you place an order with these items in your cart, it will not ship
until at least {latestPreorder}
</p>
)}
</FormController>
</>
)

@ -16,6 +16,17 @@ export default function CheckoutComplete({order}){
const items = order.transactions.map(transaction => transaction.cart.items).flat()
const latestTransaction = order.transactions.sort(sortTransactions)[0]
const preorderDates = order.transactions.filter(t => t.has_preorder).map(t => DateTime.fromISO(t.preorder_fulfill_date))
preorderDates.sort((b, a) => a < b ? -1 : a > b ? 1 : 0)
const latestPreorder = preorderDates[0]?.toLocaleString(DateTime.DATE_FULL)
let shippingEstimate = 'Orders typically are shipped within 3-5 business days of purchase.'
if (latestPreorder) {
shippingEstimate = `Your order will be shipped when all of its items become available (estimated ${latestPreorder.toLocaleString(DateTime.DATE_SHORT)}).`
}
let email = null
const stripePayment = latestTransaction.payments.find(p => p.stripe !== null)
@ -55,8 +66,7 @@ export default function CheckoutComplete({order}){
? (
<>
<p>
Orders typically are shipped within 3-5 business days of purchase. You
will receive an email with a tracking number when your order ships.
{shippingEstimate} You will receive an email with a tracking number when your order ships.
</p>
<p>
Your tracking number will be sent to: <strong>
@ -64,7 +74,7 @@ export default function CheckoutComplete({order}){
</strong>
</p>
<p>
If you do not receive an email within 1 week,
If you do not receive an email {latestPreorder ? 'within a week of the shipping estimate' : 'within 1 week'},
please <Link href="/contact"><a>contact us</a></Link>. In any
correspondence please be sure to include your order number
(#{order.number}).
@ -72,9 +82,7 @@ export default function CheckoutComplete({order}){
</>
)
: (
<p>
Orders typically are shipped within 3-5 business days of purchase.
</p>
<p>{shippingEstimate}</p>
)
}
</>

@ -8,6 +8,7 @@ import {Input, Button} from '~/components/form'
import useUser from '~/hooks/useUser'
import axios from 'axios'
import { loadStripe } from '@stripe/stripe-js';
import {DateTime} from 'luxon'
const { publicRuntimeConfig: {STRIPE_PUBLIC_KEY} } = getConfig()
const stripePromise = loadStripe(STRIPE_PUBLIC_KEY);
@ -36,6 +37,10 @@ export default function CheckoutSummary({order: _order, addresses}){
? item_total_price + (free_shipping ? 0 : shipping_price) + tax_price - (coupon_effective_discount || 0)
: null
const preorderItems = currentTransaction.cart.items.filter(({item, count}) => count > item.number_in_stock && item.preorder_availability_date).map(({item}) => DateTime.fromISO(item.preorder_availability_date)) || []
preorderItems.sort((b, a) => a < b ? -1 : a > b ? 1 : 0)
const latestPreorder = preorderItems[0]?.toLocaleString(DateTime.DATE_FULL)
const formatMoney = money => {
if (money === undefined || money === null) return null;
@ -246,6 +251,16 @@ export default function CheckoutSummary({order: _order, addresses}){
</div>
</div>
</div>
{latestPreorder && (
<div className={styles.checkoutSection}>
<p className="warning">
<strong>Note: </strong>
Your cart contains one or more pre-order items. If you complete this order, it will not ship
until at least {latestPreorder}
</p>
</div>
)}
</>
)
}

@ -3,6 +3,7 @@ import Head from 'next/head'
import Router from 'next/router'
import {FormController, IntegerInput, Button} from '~/components/form'
import isNum from 'validator/lib/isNumeric'
import {DateTime} from 'luxon'
import useCart, {useSetCart} from '~/hooks/useCart'
import styles from './style.module.css'
@ -16,6 +17,10 @@ Item.getInitialProps = async function({ctx: {axios, query: {slug}}}){
throw err;
}
if(item.preorder_availability_date) {
item.preorder_availability_date = DateTime.fromISO(item.preorder_availability_date).toLocaleString(DateTime.DATE_FULL)
}
return {item}
}
@ -29,6 +34,18 @@ export default function Item({item}){
const numInCart = cart?.items?.find(i => i.item.uuid === item.uuid)?.count || 0
const numberInStock = item.number_in_stock
const numberPreorderSlots = (item.preorder_maximum || 0) - (-Math.min(0, item.number_in_stock))
const availabilityText =
numberInStock > 0
? `${numberInStock} in stock`
: item.preorder_availability_date && numberPreorderSlots > 0
? `${numberPreorderSlots} available for pre-order`
: 'Currently out of stock'
const totalAvailable = (numberInStock > 0 ? numberInStock : numberPreorderSlots) - numInCart
return (
<>
<Head>
@ -65,9 +82,9 @@ export default function Item({item}){
{ item.description.split('\n').map(p=>p.trim()).filter(p=>p !== '').map((p,i)=><p key={i}>{p}</p>) }
<FormController className={styles.form} url={`/api/cart/add/${item.uuid}`} afterSubmit={(cart)=>{setCart(cart); Router.push('/store/cart')}}>
<IntegerInput label="Add to cart" name="count" initialValue="1" minimum="1" validate={value=>isNum(value, {no_symbols: true})} />
<IntegerInput label="Add to cart" name="count" initialValue="1" minimum="1" maximum={totalAvailable.toFixed(0)} validate={value=>isNum(value, {no_symbols: true}) && parseInt(value) <= totalAvailable} />
{
item.number_in_stock > 0
totalAvailable > 0
? <Button type="submit">Add to cart</Button>
: <Button enabled={false} type="submit">Out of stock</Button>
}
@ -77,14 +94,18 @@ export default function Item({item}){
<div className={styles.notes}>
<span>Price: ${(parseInt(item.price_cents) / 100).toFixed(2)} each</span>
<span>
{
item.number_in_stock
? item.number_in_stock + ' in stock'
: 'Out of stock'
}
{availabilityText}
</span>
<span>{numInCart} in cart</span>
<span>{numInCart} in <button style={{color: "white"}} className="buttonLink" onClick={() => Router.push(`/store/cart`)}>cart</button></span>
</div>
{numberInStock < 1 && item.preorder_availability_date && numberPreorderSlots > 0 && (
<p className="warning">
<strong>Note: </strong>
This is a pre-order item. If you place an order with this item in your cart, it will not ship
until at least {item.preorder_availability_date}
</p>
)}
</div>
</div>
<div className={styles.moreDetails}>

Loading…
Cancel
Save