diff --git a/api/orders.js b/api/orders.js
index 1263bd0..cbac1a0 100644
--- a/api/orders.js
+++ b/api/orders.js
@@ -237,3 +237,59 @@ router.post('/:uuid/ship/easypost', ensureAdmin, parseJSON, async (req, res) =>
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)
+})
\ No newline at end of file
diff --git a/components/form/form.js b/components/form/form.js
index 60b1bd1..1e019bd 100644
--- a/components/form/form.js
+++ b/components/form/form.js
@@ -164,7 +164,12 @@ export const FormController = function FormController({children, className, url,
if(child.props.hidden) return null;
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}),
value: state.fields[name].value,
isValid: state.fields[name].touched ? state.fields[name].isValid : true,
diff --git a/db/mappings/order.js b/db/mappings/order.js
index 0d8a865..076b90e 100644
--- a/db/mappings/order.js
+++ b/db/mappings/order.js
@@ -67,7 +67,8 @@ module.exports = [{
properties: [
'type',
'time',
- 'value_cents'
+ 'value_cents',
+ 'recipient_email'
],
associations: [
{name: 'stripe', mapId: 'paymentStripeMap', columnPrefix: 'stripe_'}
diff --git a/db/models/order.js b/db/models/order.js
index 690ff24..c2c807d 100644
--- a/db/models/order.js
+++ b/db/models/order.js
@@ -133,7 +133,7 @@ order.addAddress = async function (transaction, address){
// Get shipping price (as cents)
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)
throw new Error("Unable to estimate price");
@@ -278,6 +278,30 @@ order.addPayment = async function(transaction, paymentIntent){
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) =>
dbUtil.executeFunction({
name: 'set_delivery_tracking',
diff --git a/db/sql/1-tables.sql b/db/sql/1-tables.sql
index bc0a7f1..9bf1992 100644
--- a/db/sql/1-tables.sql
+++ b/db/sql/1-tables.sql
@@ -1,6 +1,6 @@
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');
+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."stock_change_dir_enum" as enum ('added', 'subtracted');
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
);
+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" (
shipment_uuid uuid primary key default uuid_generate_v4(),
shipment_date timestamptz not null default now(),
diff --git a/db/sql/2-views.sql b/db/sql/2-views.sql
index b4cd96f..3aade9a 100644
--- a/db/sql/2-views.sql
+++ b/db/sql/2-views.sql
@@ -91,10 +91,13 @@ create or replace view sos.v_payment as
select
payment.*,
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"
- 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_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_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
select
diff --git a/db/sql/3-functions.sql b/db/sql/3-functions.sql
index 9fc2fae..4ea50b3 100644
--- a/db/sql/3-functions.sql
+++ b/db/sql/3-functions.sql
@@ -886,6 +886,70 @@ begin
return query select * from sos.v_order where order_uuid = _order_uuid;
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)
returns setof sos.v_session
language plpgsql
diff --git a/pages/admin/orders/index.js b/pages/admin/orders/index.js
index afb35bc..cde3d5c 100644
--- a/pages/admin/orders/index.js
+++ b/pages/admin/orders/index.js
@@ -15,7 +15,9 @@ export default function Orders({orders}){
return (
<>
-
{item.name} {count > 1 ? `(${count})` : ''}: | +{formatMoney(count * item.price_cents)} | +
Coupon: | +- {formatMoney(coupon_effective_discount)} | +
Shipping: | +{formatMoney(shipping_price) || '-'} | +
Sales tax: | +{formatMoney(tax_price)} | +
Total: | +{formatMoney(total_price) || '-'} | +
(Paid by admin) | +{formatMoney(0) || '-'} | +