Manually add orders

main
Ashelyn Dawn 4 years ago
parent bd6dd194d6
commit f739d7a5fb

@ -237,3 +237,59 @@ router.post('/:uuid/ship/easypost', ensureAdmin, parseJSON, async (req, res) =>
res.json(updatedOrder) res.json(updatedOrder)
}) })
router.put('/manual', ensureAdmin, parseJSON, async (req, res) => {
const {items} = req.body
if(!items?.length)
throw new Error("No items in order")
const cart = await db.cart.create(null)
for(const item of items)
await db.cart.addItemToCart(cart.uuid, item.uuid, item.count)
const order = await db.order.create(cart.uuid, req.session.uuid);
res.json(order)
})
router.post('/manual/:uuid/address', ensureAdmin, parseJSON, validate.address, validate.handleApiError, async (req, res) => {
const origOrder = await db.order.findByUUID(req.params.uuid);
if(!origOrder) throw new Error("Unable to find order");
const currentTransaction = origOrder
.transactions.find(transaction => (
transaction.payment_state === 'started'
))
const {name, street1, street2, city, state, zip, country} = req.body;
// Create address, update order
const address = await db.address.create(name, street1, street2, city, state, zip, country, null)
const order = await db.order.addAddress(currentTransaction, address)
res.json(order)
})
router.post('/manual/:uuid/payment', ensureAdmin, parseJSON, validate.validEmail('recipientEmail'), validate.handleApiError, async (req, res) => {
const {recipientEmail, reason} = req.body;
const origOrder = await db.order.findByUUID(req.params.uuid)
const currentTransaction = origOrder
.transactions.find(transaction => (
transaction.payment_state === 'started'
))
const {item_total_price, shipping_price, tax_price, coupon_effective_discount} = currentTransaction
const {free_shipping} = currentTransaction.coupon || {}
const total_price =
(item_total_price && shipping_price)
? item_total_price + (free_shipping ? 0 : shipping_price) + tax_price - (coupon_effective_discount || 0)
: null
const order = await db.order.addAdminPayment(currentTransaction, total_price, recipientEmail, reason, req.user.uuid)
res.json(order)
})

@ -164,7 +164,12 @@ export const FormController = function FormController({children, className, url,
if(child.props.hidden) return null; if(child.props.hidden) return null;
return React.cloneElement(child, { return React.cloneElement(child, {
onChange: ev=>dispatch({name, value: ev.target.value}), onChange: ev=> {
if(child.props.onChange)
child.props.onChange(ev.target.value)
dispatch({name, value: ev.target.value})
},
onBlur: ev=>dispatch({name}), onBlur: ev=>dispatch({name}),
value: state.fields[name].value, value: state.fields[name].value,
isValid: state.fields[name].touched ? state.fields[name].isValid : true, isValid: state.fields[name].touched ? state.fields[name].isValid : true,

@ -67,7 +67,8 @@ module.exports = [{
properties: [ properties: [
'type', 'type',
'time', 'time',
'value_cents' 'value_cents',
'recipient_email'
], ],
associations: [ associations: [
{name: 'stripe', mapId: 'paymentStripeMap', columnPrefix: 'stripe_'} {name: 'stripe', mapId: 'paymentStripeMap', columnPrefix: 'stripe_'}

@ -133,7 +133,7 @@ order.addAddress = async function (transaction, address){
// Get shipping price (as cents) // Get shipping price (as cents)
const lowestRate = epShipment.lowestRate(['USPS']) const lowestRate = epShipment.lowestRate(['USPS'])
const price = parseFloat(lowestRate && lowestRate.retail_rate) * 100 const price = Math.floor(parseFloat(lowestRate && lowestRate.retail_rate) * 100)
if(!price) if(!price)
throw new Error("Unable to estimate price"); throw new Error("Unable to estimate price");
@ -278,6 +278,30 @@ order.addPayment = async function(transaction, paymentIntent){
return order return order
} }
order.addAdminPayment = async function(transaction, payment_value, recipient_email, reason, granter_uuid){
const order = await dbUtil.executeFunction({
name: 'add_admin_payment_to_transaction',
params: [
transaction.uuid,
payment_value,
recipient_email,
reason,
granter_uuid
],
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) => order.setTracking = (uuid, trackingCode, shipDate, price_cents) =>
dbUtil.executeFunction({ dbUtil.executeFunction({
name: 'set_delivery_tracking', name: 'set_delivery_tracking',

@ -1,6 +1,6 @@
create type sos."delivery_type_enum" as enum ('hand_shipped', 'easypost', 'hand_delivered'); 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."transaction_state_enum" as enum ('started', 'completed', 'cancelled', 'expired');
create type sos."payment_type_enum" as enum ('ks_reward', 'stripe', 'paypal', 'account_credit'); 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');
create type sos."stock_change_dir_enum" as enum ('added', 'subtracted'); 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'); create type sos."email_link_type_enum" as enum ('email_confirm', 'email_change', 'password_reset');
@ -227,6 +227,16 @@ create table sos."payment_stripe" (
stripe_receipt_number text NULL stripe_receipt_number text NULL
); );
create table sos."payment_admin_grant" (
payment_uuid uuid primary key default uuid_generate_v4(),
payment_type sos.payment_type_enum check (payment_type = 'admin_granted'),
foreign key (payment_uuid, payment_type) references sos."payment" (payment_uuid, payment_type),
payment_admin_granted_by uuid not null references sos."user" (user_uuid),
payment_admin_recipient_email citext not null,
payment_admin_reason text not null
);
create table sos."shipment" ( create table sos."shipment" (
shipment_uuid uuid primary key default uuid_generate_v4(), shipment_uuid uuid primary key default uuid_generate_v4(),
shipment_date timestamptz not null default now(), shipment_date timestamptz not null default now(),

@ -91,10 +91,13 @@ create or replace view sos.v_payment as
select select
payment.*, payment.*,
payment_stripe.stripe_payment_intent_id, payment_stripe.stripe_payment_intent_id,
payment_stripe.stripe_receipt_email payment_admin_grant.payment_admin_granted_by,
payment_admin_grant.payment_admin_reason,
coalesce(payment_stripe.stripe_receipt_email, payment_admin_grant.payment_admin_recipient_email) as payment_recipient_email
from sos."payment" from sos."payment"
left join sos."payment_ks_reward" on payment_ks_reward.payment_uuid = payment.payment_uuid and payment_ks_reward.payment_type = payment.payment_type left join sos."payment_ks_reward" on payment_ks_reward.payment_uuid = payment.payment_uuid and payment_ks_reward.payment_type = payment.payment_type
left join sos."payment_stripe" on payment_stripe.payment_uuid = payment.payment_uuid and payment_stripe.payment_type = payment.payment_type; left join sos."payment_stripe" on payment_stripe.payment_uuid = payment.payment_uuid and payment_stripe.payment_type = payment.payment_type
left join sos."payment_admin_grant" on payment_admin_grant.payment_uuid = payment.payment_uuid and payment_admin_grant.payment_type = payment.payment_type;
create or replace view sos.v_transaction_paid as create or replace view sos.v_transaction_paid as
select select

@ -886,6 +886,70 @@ begin
return query select * from sos.v_order where order_uuid = _order_uuid; return query select * from sos.v_order where order_uuid = _order_uuid;
end; $function$; end; $function$;
create or replace function sos.add_admin_payment_to_transaction(_transaction_uuid uuid, _payment_value_cents integer, _recipient_email citext, _reason text, _granter uuid)
returns setof sos.v_order
language plpgsql
as $function$
declare
_payment_uuid uuid;
_is_paid integer;
_order_uuid uuid;
begin
-- Get the transaction's order
select transaction_order_uuid into _order_uuid
from sos."transaction"
where transaction_uuid = _transaction_uuid;
if _order_uuid is null then
raise 'Transaction has no order';
end if;
insert into sos."payment" (
payment_type,
payment_value_cents,
payment_transaction_uuid
) values (
'admin_granted',
_payment_value_cents,
_transaction_uuid
) returning payment_uuid into _payment_uuid;
insert into sos."payment_admin_grant" (
payment_uuid,
payment_type,
payment_admin_granted_by,
payment_admin_recipient_email,
payment_admin_reason
) values (
_payment_uuid,
'admin_granted',
_granter,
_recipient_email,
_reason
);
select
count(*) into _is_paid
from sos.v_order
where transaction_uuid = _transaction_uuid
and transaction_computed_price <= transaction_amount_paid_cents;
if _is_paid > 0 then
update sos."transaction" set (
transaction_payment_state,
transaction_completion_time
) = (
'completed',
now()
)
where
transaction_uuid = _transaction_uuid;
end if;
return query select * from sos.v_order where order_uuid = _order_uuid;
end; $function$;
create or replace function sos.attach_cart(_session_uuid uuid, _cart_uuid uuid) create or replace function sos.attach_cart(_session_uuid uuid, _cart_uuid uuid)
returns setof sos.v_session returns setof sos.v_session
language plpgsql language plpgsql

@ -15,7 +15,9 @@ export default function Orders({orders}){
return ( return (
<> <>
<ActionBar title="Orders"/> <ActionBar title="Orders" actions={[
{label: 'Add Manual Order', url: `/admin/orders/new`}
]}/>
<h4>Unsent:</h4> <h4>Unsent:</h4>
<Table <Table
columns={[ columns={[
@ -59,7 +61,7 @@ function getPurchaseTime(order){
function getNumberItems(order){ function getNumberItems(order){
return order.transactions.map(transaction => return order.transactions.map(transaction =>
transaction.cart.items.map(item => item.count) transaction.cart.items.map(item => item.count)
).reduce((a,b)=>(a+b)) ).flat().reduce((a,b)=>(a+b))
} }
function getItemPrice(order){ function getItemPrice(order){
@ -79,9 +81,11 @@ function getActualShipping(order) {
} }
function getAmountPaid(order){ function getAmountPaid(order){
return formatMoney(order.transactions.map(({payments}) => payments.map(payment => payment.value_cents)) return formatMoney(order.transactions.map(({payments}) => payments
.filter(payment => payment.type !== 'admin_granted')
.map(payment => payment.value_cents))
.flat() .flat()
.reduce((a,b)=>(a+b))) .reduce((a,b)=>(a+b), 0))
} }
function parsePaymentTime({payments}){ function parsePaymentTime({payments}){

@ -0,0 +1,35 @@
import React from 'react'
import ActionBar from '~/components/admin/actionBar'
import {FormController, Input, Button} from '~/components/form'
import Router from 'next/router'
OrderAddress.getInitialProps = async ({ctx}) => {
const {data: order} = await ctx.axios.get(`/api/orders/${ctx.query.uuid}`)
return {order}
}
export default function OrderAddress({order}) {
function afterSave(order) {
Router.push(`/admin/orders/new/${order.uuid}/payment`)
}
const {address} = order;
return (
<>
<ActionBar title="Set Address" actions={[
{label: 'Cancel', url: `/admin/orders`}
]}/>
<FormController url={`/api/orders/manual/${order.uuid}/address`} afterSubmit={afterSave}>
<Input initialValue={address?.name} name="name" validate={value=>value.length > 0}/>
<Input initialValue={address?.street1} label="Street (line 1)" name="street1" validate={value=>value.length > 0}/>
<Input initialValue={address?.street2} label="Street (line 2)" name="street2"/>
<Input initialValue={address?.city} label="City" name="city" validate={value=>value.length > 0}/>
<Input initialValue={address?.state} label="State / Province" name="state" validate={value=>value.length > 0}/>
<Input initialValue={address?.zip} label="Postal Code" name="zip" validate={value=>value.length > 0}/>
<Input initialValue={address?.country} label="Country" name="country" validate={value=>value.length > 0}/>
<Button type="submit">Save Address</Button>
</FormController>
</>
)
}

@ -0,0 +1,108 @@
import React, {useState} from 'react'
import Router from 'next/router'
import axios from 'axios'
import ActionBar from '~/components/admin/actionBar'
import {Button, FormController, Input} from '~/components/form'
import OrderSummary from '~/components/orderSummary'
import styles from '../../../../store/checkout/style.module.css'
ManualOrderPayment.getInitialProps = async ({ctx}) => {
const {data: order} = await ctx.axios.get(`/api/orders/${ctx.query.uuid}`)
return {order}
}
export default function ManualOrderPayment({order}) {
const [recipientEmail, setRecipientEmail] = useState('')
const [orderNote, setOrderNote] = useState('')
const validToSubmit = recipientEmail.length > 0 && orderNote.length > 0
const currentTransaction = order
.transactions.find(transaction => (
transaction.payment_state === 'started'
))
const {item_total_price, shipping_price, tax_price, coupon_effective_discount} = currentTransaction
const {free_shipping} = currentTransaction.coupon || {}
const total_price =
(item_total_price && shipping_price)
? item_total_price + (free_shipping ? 0 : shipping_price) + tax_price - (coupon_effective_discount || 0)
: null
const formatMoney = money => {
if (money === undefined || money === null) return null;
return '$' + (money / 100).toFixed(2)
}
async function markPaid() {
await axios.post(`/api/orders/manual/${order.uuid}/payment`, {
recipientEmail,
reason: orderNote
})
Router.push('/admin/orders')
}
return (
<>
<ActionBar title="Set Payment" actions={[
{label: 'Edit address', url: `/admin/orders/new/${order.uuid}/address`},
{label: 'Cancel', url: `/admin/orders`}
]}/>
<OrderSummary order={order} />
<FormController>
<Input label="Recipient email" name="email" validate={value=>value.length > 0} onChange={setRecipientEmail} />
<Input label="Order explanation" name="explanation" validate={value=>value.length > 0} onChange={setOrderNote} />
</FormController>
<div className={styles.checkoutSection}>
<h3>Price Total:</h3>
<div className={styles.horizContainer}>
<div>
<table>
<tbody>
{currentTransaction.cart.items.map(({uuid, count, item}) => (
<tr key={uuid}>
<td>{item.name} {count > 1 ? `(${count})` : ''}:</td>
<td>{formatMoney(count * item.price_cents)}</td>
</tr>
))}
{coupon_effective_discount > 0 && (
<tr>
<td>Coupon:</td>
<td>- {formatMoney(coupon_effective_discount)}</td>
</tr>
)}
<tr className={free_shipping ? 'strikethrough' : undefined}>
<td>Shipping:</td>
<td>{formatMoney(shipping_price) || '-'}</td>
</tr>
{tax_price && (
<tr>
<td>Sales tax:</td>
<td>{formatMoney(tax_price)}</td>
</tr>
)}
<tr style={{fontWeight: 'bold'}}>
<td>Total:</td>
<td style={{textDecoration: 'line-through'}}>{formatMoney(total_price) || '-'}</td>
</tr>
<tr>
<td style={{opacity: .6}}>(Paid by admin)</td>
<td style={{fontWeight: 'bold'}}>{formatMoney(0) || '-'}</td>
</tr>
</tbody>
</table>
</div>
<div className={styles.paymentButtons}>
<Button enabled={validToSubmit} onClick={markPaid}>Complete Order</Button>
</div>
</div>
</div>
</>
)
}

@ -0,0 +1,86 @@
import React, {useReducer} from 'react'
import Router from 'next/router'
import axios from 'axios'
import ActionBar from '~/components/admin/actionBar'
import Table from '~/components/table'
import {Button} from '~/components/form'
ManualOrder.getInitialProps = async ({ctx}) => {
const {data: items} = await ctx.axios.get('/api/items?showUnpublished=true')
return {items}
}
export default function ManualOrder({items}) {
const [itemState, dispatch] = useReducer(itemCountReducer, {})
const totalNumber = Object.values(itemState).reduce((a,b) => (b+a), 0)
const add = (item) => () => dispatch({type: 'add', item: item.uuid})
const subtract = (item) => () => dispatch({type: 'subtract', item: item.uuid})
const remove = (item) => () => dispatch({type: 'remove', item: item.uuid})
const clear = () => dispatch({type: 'clear'})
async function createOrder(){
const items = Object.keys(itemState)
.map(uuid => ({ uuid, count: itemState[uuid] }))
const {data: order} = await axios.put(`/api/orders/manual`, {items})
Router.push(`/admin/orders/new/${order.uuid}/address`)
}
return (
<>
<ActionBar title="New Order" actions={[
{label: 'Cancel', url: `/admin/orders`}
]}/>
<Table
columns={[
{name: 'Name', extractor: item => item.name},
{name: 'Price', extractor: item => `$${(item.price_cents / 100).toFixed(2)}`},
{name: 'Number in stock', extractor: item => item.number_in_stock},
{name: 'Number in order', extractor: item => (
<span style={{display: 'flex', flexDirection: 'row', width: 120, justifyContent: 'space-between', alignItems: 'center'}}>
<Button enabled={itemState[item.uuid] > 0} style={{width: 30}} onClick={subtract(item)}>-</Button>
{itemState[item.uuid] || 0}
<Button enabled={(itemState[item.uuid] || 0) < item.number_in_stock} style={{width: 30}} onClick={add(item)}>+</Button>
</span>
)},
{name: 'Actions', extractor: item => (
<span>
<button className="buttonLink" onClick={remove(item)}>Remove all</button>
</span>
)}
]}
// Map in an id property so the table can use array.map
rows={items.map(item => ({id: item.uuid, ...item}))}
/>
<p style={{textAlign: 'center'}}>
<button className="buttonLink" onClick={clear}>Reset order</button>
</p>
<p style={{maxWidth: 400, margin: '0 auto'}}>
<Button enabled={totalNumber > 0} onClick={createOrder}>Create Order</Button>
</p>
</>
);
}
function itemCountReducer(state, action) {
switch(action.type) {
case 'add':
if (state[action.item])
return {...state, [action.item]: state[action.item] + 1}
else
return {...state, [action.item]: 1}
case 'subtract':
if (state[action.item])
return {...state, [action.item]: state[action.item] - 1}
else
return state
case 'remove':
const newState = {...state}
delete newState[action.item]
return newState;
case 'clear':
return {};
}
}

@ -20,7 +20,7 @@ export default function CheckoutComplete({order}){
const stripePayment = latestTransaction.payments.find(p => p.stripe !== null) const stripePayment = latestTransaction.payments.find(p => p.stripe !== null)
if(stripePayment) if(stripePayment)
email = stripePayment.stripe.receipt_email email = stripePayment.recipient_email
return ( return (
<> <>

Loading…
Cancel
Save