AMKB7ZH5QDLTGTGJ63BXZKPVYZN7VWUDBRTOBJ2ZE4TS5QOWCGUQC
PUUFKW5ZJIJBWTMNX46PW4APAYBAYOAKXHSNGBIGRXCD5Z2FBM2QC
DSVZLJ3ULDNYRUVWRIHGQSPZYXQ26UGXPPNIIQGKVSKOPWTPZCLQC
XBHLXAKNBJNWSXZMHVKDMDVI7CUF5MN22FFS3XQIOLFDJHDCLTVAC
HVE3ANDORAJ5EX3I3GMSWT3INIKS6QRWPWGA6V6WY3NRCGVRFSRAC
O6OZUOIH5U2SE3FS4ZUHM755FNYXEDXSRXREAEFZ36VNII4CPRJAC
376FAPWKGZ4NT47ODONG6VIVEISEKNRHI6AC5AFS4VRJDYQHPEFQC
U7BZVX5VS4IIY7CH6HFIYXK7UQBYYUN5VYVO45LVESF2PMW7A5VAC
VO5G3FF3NDOKCIF2OU7G257R3XIPTFDCAMDTOVKHKOB5Z3I55KDQC
MPLKJQVEPZB7GEWAKZOOIKBKWHTNQX3LLESCQEG4PE3CUHQEYUBAC
5XHBBUBSPTNNPEZZVELCS5AEAKQVHEMLAI2URKBMMBUGWTPARFVQC
LUE57OK3OFZYP7GW4FL6SBV2MCBBEKQ2OFGKZSZ77AOBI6OKGDYQC
E4HFJ4L4PAGV7R7EXVG2B2CWDGGW7XWU4D37VURZ66HZ3ILHCFUAC
ZI2RJOZ2HXBHX7L54BTSDKESY6NG5KBJLVHCNY7K7ZLGIUOIHQUQC
76HUJPOPXCN2NMOWLI6JOY7HR35EL4IBHMES3DBJVCFZRKKPGQSAC
QQAJ5O5EW6ASWLXQ627Y2F6YGMYKCM4WS37LTZYOMNPPFDPMNBUAC
2Y4DI4H5EZOGS2ZUL7SXQHYUYFWWJOJPRAF4MI2RFI6YCLAJVFJAC
JP2LJ6B34S4IS6MPKF5LMMYUWIYMGU3JCJRRR4PTZ4IWM653JCXAC
RAN5QU4U2OVNLDGZVVTJNCXDN5GNAVPXGKHADZO2ML3Y5KLU4QPQC
Y4W4MACPKH3IVO6JIQYGN4V5LR6EM6RPZEJ4RHH6ZBZUDA63AGRQC
CLMZIURF2TXSMQ6YZYXR5CHGMRW6V64YFQLRCP3H3I3XB2KKOJ6AC
SE6MCCXTIXSGMAH5EL7EPDXIQKV6THCCY2H4OQJJQOHCUQAWE5VQC
7XVW32MMBBIGWQZ3QRD365TBIJU4DYN6T4VO2E46WFDCFHGO2BMAC
WYTMZJFYVKHR4QH7AV5JUNWXT6NAC5NNQNPZCQSDI6LGI7DVXFYAC
UXLEHDHRPKDY6DXD2Q64ULMVTPOPMQW6VAOPHG5K5HX24YPGG3MQC
BXGNP4GARIQA5LT3AN2F22HHGJHLUOL3YKBWWJ6LQQKUWLPYB7OAC
5SAL2YA2IIYRBZSNM6SBKHSD6UFP67GDOR5J6W2WXREYLJZEFJ6AC
SRIPJD5O5RNXAH2EOCTVJRGFSNNC4LDPBB44AE7FPEXGTC66NYTAC
Z7XOXXDJ7T6E4ZCP2AK5YSXWE6TCDCLZEXAZPZZVLOY4WCFVZPHAC
7RKEQK2KPXJ2MEHPVSY7ROMHW6FTTKCXSMZV5MEXPFZKESZQYUUAC
63VXWIHIAKGK7J4VTNRUAG2V32N2QUSWFELB6GD34S54FGRWAPCQC
MM3HQWNEUZVOGQ3WWBTECBY6F47SN4U43I5UNDOAFWO3R7U5XPBAC
2FPZGFF5PCGNV45HOB6TROMJFPURHPXM7YSPWNXCHFJS7EICVKQAC
HFOZWZPGUIVA7QUO7VTPZ2R5ADEDGG7EGL3E5TFUUYRNAOVMHIDQC
VIGS24FLOPPTBLIBZIB2LFMYCDZ3AO4FBXYADAKQMJFNPM4JHAGQC
UCY5PVFCT2QIOGR47KFS4KBF2ZPHRBOAIOH6PJEVWCHLMBRBLOMQC
--values (_user_id, (select store_id from menu_item where menu_item_id = _menu_item_id)) to make early exit and throw not null exception?? format error message in backend. are there side effects other than incremented cart_id serial?
select _user_id, store_id from menu_item
where not exists (select 1 from s) and menu_item_id = _menu_item_id
on conflict (user_id, store_id) where status <= 'ready' do nothing
select _user_id, store_id
from menu_item join store using (store_id)
where
not exists (select from s) and
menu_item_id = _menu_item_id and
partner
select * from s union select * from i
), cc as (
--condition is wrong! consider the case where a user adds n menu items from the same store simulatenously (n tabs press add2cart same time, or restore expired cart client side will send n modifycart, first cart gets created, but some others may not see the cart yet since parallel(can fix by having another endpoint that takes in multi, or waits for first to return and then execute others)) maybe solution is do update set status = 'active' where status = 'active'(empty update)??
select case when not exists (select 1 from c) and _user_id is not null then f_raise('exception', 'failed to add to cart. you already have an order in progress with this store.') end
select * from s union all select * from i
select cart_id, _menu_item_id, qty from c full join cc on true--https://dba.stackexchange.com/a/69650 force evaluation of cte
--1 * 0 => 1 row, fine since we're selecting what we want
--0 * 1 => 1 row, fine since the exception will skip the insert anyways. before insert was skipped due to 0 rows, but this is equivalent? wait no... if invalid menu_item_id, insert will still run, but caught by valid_line_item_as_of_now(). I'd much rather make 0 * 0 though... don't really care about that?
select cart_id, _menu_item_id, qty from c
$$ language sql stable;
create or replace function reservation(_cart_id integer) returns table (
store_id integer,
store_name text,
statuses json,
taxes json,
line_items json
) as $$
with blah as (
select *
from cart
where
cart_id = _cart_id and
user_id is null and
status > 'active'
), blah2 as (
select * from blah union all
select * from cart_history
where
exists (select * from blah) and
cart_id = _cart_id and
user_id is null and
status > 'active'
)
select
first(cart.store_id),
first(store__as_of.name),
first(statuses),
(select json_agg(json_build_object(
'name', name,
'rate', rate::text,
'types', types
)) from store_tax__as_of(first(cart.system_time_start)) where store_id = first(cart.store_id) and (types is null or types && array_agg(product__as_of.type))),
json_agg(json_strip_nulls(json_build_object(
'product_id', product_id,
'variant_id', variant_id,
'menu_item', menu_item_id,
'producer', producer,
'brand', brand,
'name', product__as_of.display_name,
'type', product__as_of.type,
'strain', strain__as_of.display_name,
'species', coalesce(product__as_of.species, strain__as_of.species),
'blend', blend,
'portions', portions,
'quantity', variant__as_of.quantity::text,
'flavor', flavor,
'gram_equivalency', gram_equivalency::text,
'thc', thc,
'cbd', cbd,
'price', price::text,
'howmany', line_item.quantity,
'image', (
select distinct on(product_id)--https://dba.stackexchange.com/a/159899
destination
from
product_image__as_of(cart.system_time_start)
where product_image__as_of.product_id = product__as_of.product_id
--order by rank
)
)))
from
(select
cart_id,
first(store_id) as store_id,
min(system_time_start) as system_time_start,--system_time_start of 'placed' or 'paid' since that's what the customer has agreed to. right now is just 'placed'
json_agg(json_build_object(
'status', status,
'as_of', system_time_start,
'end', system_time_end
) order by
system_time_start
) as statuses
from blah2
group by cart_id) as cart
inner join store__as_of(cart.system_time_start) using (store_id)
inner join line_item using (cart_id)
inner join menu_item__as_of(cart.system_time_start) using (menu_item_id)
inner join variant__as_of(cart.system_time_start) using (variant_id)
inner join product__as_of(cart.system_time_start) using (product_id)
left outer join strain__as_of(cart.system_time_start) using (strain_id)
group by cart_id
)) from store_tax__as_of(first(cart.system_time_start)) where store_id = first(cart.store_id)),
)) from store_tax__as_of(first(cart.system_time_start)) where store_id = first(cart.store_id) and (types is null or types && array_agg(product__as_of.type))),
(select
cart_id,
row_number() OVER (ORDER BY case
when last(status order by system_time_start) = 'ready' then 2
when last(status order by system_time_start) = 'placed' then 1
else 0
end desc,
max(system_time_start) desc) AS rn,
first(store_id) as store_id,
min(system_time_start) as system_time_start,--system_time_start of 'placed' or 'paid' since that's what the customer has agreed to. right now is just 'placed'
json_agg(json_build_object(
'status', status,
'as_of', system_time_start,
'end', system_time_end
) order by
system_time_start
) as statuses
from cart_with_history
where
user_id = _user_id and
status >= 'placed'
group by cart_id
order by
case
when last(status order by system_time_start) = 'ready' then 2
when last(status order by system_time_start) = 'placed' then 1
else 0
end desc,
max(system_time_start) desc
) as cart
(select
cart_id,
row_number() OVER (ORDER BY case
when last(status order by system_time_start) = 'ready' then 2
when last(status order by system_time_start) = 'placed' then 1
else 0
end desc,
max(system_time_start) desc) AS rn,
first(store_id) as store_id,
min(system_time_start) as system_time_start,--system_time_start of 'placed' or 'paid' since that's what the customer has agreed to. right now is just 'placed'
json_agg(json_build_object(
'status', status,
'as_of', system_time_start,
'end', system_time_end
) order by
system_time_start
) as statuses
from cart_with_history
where
user_id = _user_id and
status >= 'placed'
group by cart_id
order by
case
when last(status order by system_time_start) = 'ready' then 2
when last(status order by system_time_start) = 'placed' then 1
else 0
end desc,
max(system_time_start) desc
) as cart
)) from store_tax__as_of(first(cart.system_time_start)) where store_id = _store_id),
)) from store_tax__as_of(first(cart.system_time_start)) where store_id = _store_id and (types is null or types && array_agg(product__as_of.type))),
(select
cart_id,
row_number() OVER (ORDER BY case
when last(status order by system_time_start) = 'placed' then 2
when last(status order by system_time_start) = 'ready' then 1
else 0
end desc,
max(system_time_start) desc) AS rn,
first(user_id) as user_id,
min(system_time_start) as system_time_start,--system_time_start of 'placed' or 'paid' since that's what the customer has agreed to. right now is just 'placed'
json_agg(json_build_object(
'status', status,
'as_of', system_time_start,
'end', system_time_end
) order by
system_time_start
) as statuses
from cart_with_history
where
store_id = _store_id and
status >= 'placed'
group by cart_id
order by--maybe this is unnecessary?
case
when last(status order by system_time_start) = 'placed' then 2
when last(status order by system_time_start) = 'ready' then 1
else 0
end desc,
max(system_time_start) desc
) as cart
inner join usr__as_of(cart.system_time_start) using (user_id)
(select
cart_id,
row_number() OVER (ORDER BY case
when last(status order by system_time_start) = 'placed' then 2
when last(status order by system_time_start) = 'ready' then 1
else 0
end desc,
max(system_time_start) desc) AS rn,
min(system_time_start) as system_time_start,--system_time_start of 'placed' or 'paid' since that's what the customer has agreed to. right now is just 'placed'
json_agg(json_build_object(
'status', status,
'as_of', system_time_start,
'end', system_time_end
) order by
system_time_start
) as statuses
from cart_with_history
where
store_id = _store_id and
status >= 'placed'
group by cart_id
order by--maybe this is unnecessary?
case
when last(status order by system_time_start) = 'placed' then 2
when last(status order by system_time_start) = 'ready' then 1
else 0
end desc,
max(system_time_start) desc
) as cart
async function mergeCart(client, carts, ws) {
let decryptedCarts = [];
if(Array.isArray(carts)) {
for(let i = 0; i < carts.length; ++i) {
try {
decryptedCarts.push(cryptids.s2i(carts[i]));
} catch(e) {}
}
}
if(decryptedCarts.length) {
let diy = false;
if(!client) {
diy = true;
client = await pool.connect();
await client.query('begin');
}
let carts2merge = (await client.query(`
with distinct_cart_per_store as (
select distinct on (store_id) cart_id, store_id
from cart
where cart_id = any($2) and user_id is null and status = 'active'
),
associate_user2cart as (
update cart
set user_id = $1
where
cart_id in (select cart_id from distinct_cart_per_store) and
not exists(select from cart as c where c.user_id = $1 and c.store_id = cart.store_id and c.status = 'active')
returning cart_id, store_id
)
select
t.cart_id as anon_cart_id,
cart.cart_id as user_cart_id,
json_agg(json_build_object(
'line_item_id', line_item_id,
'menu_item_id', menu_item_id,
'quantity', quantity
)) as line_items2associate_user_or_add_qty
from
(select * from distinct_cart_per_store except all select * from associate_user2cart) as t inner join
cart on (t.store_id = cart.store_id and status = 'active' and user_id = $1) inner join
line_item on (t.cart_id = line_item.cart_id)
group by
t.cart_id,
cart.cart_id
`, [ws.user_ID, decryptedCarts])).rows;
//todo: set based merge cart or cursor plpgsql function. this is ugly
loop1: for(let i = 0; i < carts2merge.length; ++i) {
let addQty = [];
let lis = carts2merge[i].line_items2associate_user_or_add_qty;
for(let j = 0; j < lis.length; ++j) {
try {
await client.query('savepoint wow');
await client.query(`update line_item set cart_id = $1 where line_item_id = $2`, [carts2merge[i].user_cart_id, lis[j].line_item_id]);
} catch(e) {
await client.query('rollback to wow');
if(e.constraint === 'line_item_cart_id_menu_item_id_key') {
addQty.push(lis[j]);
} else if(e.message === 'exceeded 30g limit') {
continue loop1;
} else {
console.error(e);
}
}
}
console.log('sanity');
await client.query('delete from cart where cart_id = $1', [carts2merge[i].anon_cart_id]);
for(let j = 0; j < addQty.length; ++j) {
try {
await client.query('savepoint wow');
await client.query('update line_item set quantity = quantity + $1 where cart_id = $2 and menu_item_id = $3', [addQty[j].quantity, carts2merge[i].user_cart_id, addQty[j].menu_item_id]);
} catch(e) {
await client.query('rollback to wow');
if(e.message === 'exceeded 30g limit') {
break;
} else {
console.error(e);
}
}
}
}
if(diy) {
await client.query('commit');
client.release();
}
}
}
if(hack && Array.isArray(ws.q.carts) && (ws.q.carts = ws.q.carts.map(Number)).every(Number.isInteger)) {
const client = await pool.connect();
let m = (await client.query(`
with f as (
select distinct on (store_id) cart_id, store_id
from cart
where cart_id = any($2) and user_id is null and status = 'active'
),
r as (
update cart
set user_id = $1
where
cart_id in (select cart_id from f) and
not exists(select 1 from cart as c where c.user_id = $1 and c.store_id = cart.store_id and status <= 'ready')
returning cart_id, store_id
)
select * from f except select * from r
`, [ws.user_ID, ws.q.carts])).rows;
//todo: set based merge cart or cursor plpgsql function. this is ugly
for(let i = 0; i < m.length; ++i) {
let c = (await client.query({
text: `select cart_id from cart where user_id = $1 and store_id = $2 and status = 'active'`,
values: [ws.user_ID, m[i].store_id],
rowMode: 'array'
})).rows?.[0]?.[0];
if(c) {
let li = (await client.query({
text: 'select line_item_id, menu_item_id, quantity from line_item where cart_id = $1',
values: [m[i].cart_id],
rowMode: 'array'
})).rows;
let later = [];
for(let j = 0; j < li.length; ++j) {
try {
await client.query('savepoint wow');
await client.query(`update line_item set cart_id = $1 where line_item_id = $2`, [c, li[j][0]]);
} catch(e) {
await client.query('rollback to wow');
if(e.constraint === 'line_item_cart_id_menu_item_id_key') {
later.push(li[j]);
} else if(e.message === 'exceeded 30g limit') {
later = [];
break;
} else {
console.error(e);
}
}
}
for(let j = 0; j < later.length; ++j) {
try {
await client.query('savepoint wow');
await client.query('update line_item set quantity = quantity + $1 where cart_id = $2 and menu_item_id = $3', [later[j][2], c, later[j][1]]);
} catch(e) {
await client.query('rollback to wow');
if(e.message === 'exceeded 30g limit') {
break;
} else {
console.error(e);
}
}
}
} else {
console.log(`failed to merge cart. reservation in progress with store ${m[i].store_id}`);
}
}
await client.query('delete from cart where cart_id = any($1)', [m]);
await client.query('commit');
client.release();
}
delete ws.q;
html: `<a href="https://www.lobojane.com/user?otp=${token}">idk</a><div>Alternatively, copy and paste <code>${token}</code> into the input field</div>if this wasn't you, let us know by replying to this email.`
html: `<a href="http://localhost:3000?otp=${token}">idk</a><div>Alternatively, copy and paste <code>${token}</code> into the input field</div>if this wasn't you, let us know by replying to this email.`
if(Array.isArray(parameters.carts) && (parameters.carts = parameters.carts.map(Number)).every(Number.isInteger)) {
const client = await pool.connect();
let m = (await client.query(`
with f as (
select distinct on (store_id) cart_id, store_id
from cart
where cart_id = any($2) and user_id is null and status = 'active'
),
r as (
update cart
set user_id = $1
where
cart_id in (select cart_id from f) and
not exists(select 1 from cart as c where c.user_id = $1 and c.store_id = cart.store_id and status <= 'ready')
returning cart_id, store_id
)
select * from f except select * from r
`, [user.user_id, parameters.carts])).rows;
//todo: set based merge cart or cursor plpgsql function. this is ugly
for(let i = 0; i < m.length; ++i) {
let c = (await client.query({
text: `select cart_id from cart where user_id = $1 and store_id = $2 and status = 'active'`,
values: [user.user_id, m[i].store_id],
rowMode: 'array'
})).rows?.[0]?.[0];
if(c) {
let li = (await client.query({
text: 'select line_item_id, menu_item_id, quantity from line_item where cart_id = $1',
values: [m[i].cart_id],
rowMode: 'array'
})).rows;
let later = [];
for(let j = 0; j < li.length; ++j) {
try {
await client.query('savepoint wow');
await client.query(`update line_item set cart_id = $1 where line_item_id = $2`, [c, li[j][0]]);
} catch(e) {
await client.query('rollback to wow');
if(e.constraint === 'line_item_cart_id_menu_item_id_key') {
later.push(li[j]);
} else if(e.message === 'exceeded 30g limit') {
later = [];
break;
} else {
console.error(e);
}
}
}
for(let j = 0; j < later.length; ++j) {
try {
await client.query('savepoint wow');
await client.query('update line_item set quantity = quantity + $1 where cart_id = $2 and menu_item_id = $3', [later[j][2], c, later[j][1]]);
} catch(e) {
await client.query('rollback to wow');
if(e.message === 'exceeded 30g limit') {
break;
} else {
console.error(e);
}
}
}
} else {
console.log(`failed to merge cart. reservation in progress with store ${m[i].store_id}`);
}
}
await client.query('delete from cart where cart_id = any($1)', [m]);
await client.query('commit');
client.release();
}
await mergeCart(null, parameters.carts, ws);
ws.user_ID = user.user_id;
let token = await randomBytes(16);
const client = await pool.connect();
await client.query('update usr set token_hash = $1 where user_id = $2', [blake3(token, {length: 16}), user.user_id]);
if(Array.isArray(parameters.carts) && (parameters.carts = parameters.carts.map(Number)).every(Number.isInteger)) {
let m = (await client.query(`
with f as (
select distinct on (store_id) cart_id, store_id
from cart
where cart_id = any($2) and user_id is null and status = 'active'
),
r as (
update cart
set user_id = $1
where
cart_id in (select cart_id from f) and
not exists(select 1 from cart as c where c.user_id = $1 and c.store_id = cart.store_id and status <= 'ready')
returning cart_id, store_id
)
select * from f except select * from r
`, [user.user_id, parameters.carts])).rows;
//todo: set based merge cart or cursor plpgsql function. this is ugly
for(let i = 0; i < m.length; ++i) {
let c = (await client.query({
text: `select cart_id from cart where user_id = $1 and store_id = $2 and status = 'active'`,
values: [user.user_id, m[i].store_id],
rowMode: 'array'
})).rows?.[0]?.[0];
if(c) {
let li = (await client.query({
text: 'select line_item_id, menu_item_id, quantity from line_item where cart_id = $1',
values: [m[i].cart_id],
rowMode: 'array'
})).rows;
let later = [];
for(let j = 0; j < li.length; ++j) {
try {
await client.query('savepoint wow');
await client.query(`update line_item set cart_id = $1 where line_item_id = $2`, [c, li[j][0]]);
} catch(e) {
await client.query('rollback to wow');
if(e.constraint === 'line_item_cart_id_menu_item_id_key') {
later.push(li[j]);
} else if(e.message === 'exceeded 30g limit') {
later = [];
break;
} else {
console.error(e);
}
}
}
for(let j = 0; j < later.length; ++j) {
try {
await client.query('savepoint wow');
await client.query('update line_item set quantity = quantity + $1 where cart_id = $2 and menu_item_id = $3', [later[j][2], c, later[j][1]]);
} catch(e) {
await client.query('rollback to wow');
if(e.message === 'exceeded 30g limit') {
break;
} else {
console.error(e);
}
}
}
} else {
console.log(`failed to merge cart. reservation in progress with store ${m[i].store_id}`);
}
}
await client.query('delete from cart where cart_id = any($1)', [m]);
}
await client.query('commit');
client.release();
//yuck, since json can't send binary, need to hex encode. fyi base64 is smaller than hex(base16), but after compressing, hex is smaller. also base64 has issues with regards to url encoding, and slower than hex
//if remember me on client side, store in localstorage. else do nothing with token
//on subsequent new sockets, auto_login with token. token will be invalid(undefined) if remember me was not checked
ws.send(JSON.stringify({
response_ID: request_ID,
data: {token: token.toString('hex'), god: user.god, acl: user.acl}
}));
let old = authenticated.get(ws.user_ID);
authenticated.set(ws.user_ID, ws);
if(old) {
old.send(JSON.stringify({
what: 'user',
how: 'replace',
data: null,
why: 'logged in elsewhere'
}));
old.end(1000, 'logged in elsewhere');//this works correctly in firefox onclose, but not in chrome. https://stackoverflow.com/a/53340067 todo: report on chromium bug tracker
} else {
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));
}
//
ws.user_ID = user.user_id;
let token = await randomBytes(16);
const client = await pool.connect();
await client.query('update usr set token_hash = $1, passphrase_hash = $3, webportal_hash = null where user_id = $2', [blake3(token, {length: 16}), user.user_id, await argon2.hash(parameters.passphrase)]);
if(Array.isArray(parameters.carts) && (parameters.carts = parameters.carts.map(Number)).every(Number.isInteger)) {
let m = (await client.query(`
with f as (
select distinct on (store_id) cart_id, store_id
from cart
where cart_id = any($2) and user_id is null and status = 'active'
),
r as (
update cart
set user_id = $1
where
cart_id in (select cart_id from f) and
not exists(select 1 from cart as c where c.user_id = $1 and c.store_id = cart.store_id and status <= 'ready')
returning cart_id, store_id
)
select * from f except select * from r
`, [user.user_id, parameters.carts])).rows;
//todo: set based merge cart or cursor plpgsql function. this is ugly
for(let i = 0; i < m.length; ++i) {
let c = (await client.query({
text: `select cart_id from cart where user_id = $1 and store_id = $2 and status = 'active'`,
values: [user.user_id, m[i].store_id],
rowMode: 'array'
})).rows?.[0]?.[0];
if(c) {
let li = (await client.query({
text: 'select line_item_id, menu_item_id, quantity from line_item where cart_id = $1',
values: [m[i].cart_id],
rowMode: 'array'
})).rows;
let later = [];
for(let j = 0; j < li.length; ++j) {
try {
await client.query('savepoint wow');
await client.query(`update line_item set cart_id = $1 where line_item_id = $2`, [c, li[j][0]]);
} catch(e) {
await client.query('rollback to wow');
if(e.constraint === 'line_item_cart_id_menu_item_id_key') {
later.push(li[j]);
} else if(e.message === 'exceeded 30g limit') {
later = [];
break;
} else {
console.error(e);
}
}
}
for(let j = 0; j < later.length; ++j) {
try {
await client.query('savepoint wow');
await client.query('update line_item set quantity = quantity + $1 where cart_id = $2 and menu_item_id = $3', [later[j][2], c, later[j][1]]);
} catch(e) {
await client.query('rollback to wow');
if(e.message === 'exceeded 30g limit') {
break;
} else {
console.error(e);
}
}
}
} else {
console.log(`failed to merge cart. reservation in progress with store ${m[i].store_id}`);
}
}
await client.query('delete from cart where cart_id = any($1)', [m]);
}
await client.query('commit');
client.release();
//yuck, since json can't send binary, need to hex encode. fyi base64 is smaller than hex(base16), but after compressing, hex is smaller. also base64 has issues with regards to url encoding, and slower than hex
//if remember me on client side, store in localstorage. else do nothing with token
//on subsequent new sockets, auto_login with token. token will be invalid(undefined) if remember me was not checked
ws.send(JSON.stringify({
response_ID: request_ID,
data: {token: token.toString('hex'), god: user.god, acl: user.acl}
}));
let old = authenticated.get(ws.user_ID);
authenticated.set(ws.user_ID, ws);
if(old) {
old.send(JSON.stringify({
what: 'user',
how: 'replace',
data: null,
why: 'logged in elsewhere'
}));
old.end(1000, 'logged in elsewhere');//this works correctly in firefox onclose, but not in chrome. https://stackoverflow.com/a/53340067 todo: report on chromium bug tracker
} else {
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));
}
q = ', passphrase_hash = $3, webportal_hash = null';
p.push(await argon2.hash(parameters.passphrase));
ws.user_ID = user.user_id;
let token = await randomBytes(16);
const client = await pool.connect();
await client.query('begin');
await client.query(`update usr set token_hash = $1${q} where user_id = $2`, [blake3(token, {length: 16}), ws.user_id].concat(p));
await mergeCart(client, parameters.carts, ws);
await client.query('commit');
client.release();
//yuck, since json can't send binary, need to hex encode. fyi base64 is smaller than hex(base16), but after compressing, hex is smaller. also base64 has issues with regards to url encoding, and slower than hex
//if remember me on client side, store in localstorage. else do nothing with token
//on subsequent new sockets, auto_login with token. token will be invalid(undefined) if remember me was not checked
ws.send(JSON.stringify({
response_ID: request_ID,
data: {token: token.toString('hex'), god: user.god, acl: user.acl}
}));
let old = authenticated.get(ws.user_ID);
authenticated.set(ws.user_ID, ws);
if(old) {
old.send(JSON.stringify({
what: 'user',
how: 'replace',
data: null,
why: 'logged in elsewhere'
}));
old.end(1000, 'logged in elsewhere');//this works correctly in firefox onclose, but not in chrome. https://stackoverflow.com/a/53340067 todo: report on chromium bug tracker
} else {
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));
}
} else if(Array.isArray(parameters.carts) && parameters.carts.every(c => Number.isInteger(Number(c)))) {
qs.push(`cart_ids => $${params.push(parameters.carts)}`);
} else if(Array.isArray(parameters.carts)) {
let decryptedCarts = [];
for(let i = 0; i < parameters.carts.length; ++i) {
try {
decryptedCarts.push(cryptids.s2i(parameters.carts[i]));
} catch(e) {}
}
if(decryptedCarts.length) {
qs.push(`cart_ids => $${params.push(decryptedCarts)}`);
}
} else if(parameters && Array.isArray(parameters.carts) && parameters.carts.every(c => Number.isInteger(Number(c)))) {
} else if(Array.isArray(parameters.carts)) {
let data = 'invalid cart';
let decryptedCarts = [];
for(let i = 0; i < parameters.carts.length; ++i) {
try {
decryptedCarts.push(cryptids.s2i(parameters.carts[i]));
} catch(e) {}
}
if(decryptedCarts.length) {
data = (await pool.query(`select * from anonymous_carts($1)`, [decryptedCarts])).rows;
for(let i = 0; i < data.length; ++i) {
data[i].cart_id = cryptids.i2s(data[i].cart_id);
}
}
case 'reserve':
if(ws.user_ID) {
let data = await pool.query(`update cart set status = 'placed' where cart.user_id = $1 and cart.status = 'active' and cart.store_id = $2 returning system_time_start, cart_id`, [ws.user_ID, parameters.store]);
case 'reservation': {
let kartik;
try {
kartik = cryptids.s2i(parameters.cart);
} catch(e) {
if(data.rowCount === 1) {
let email = pool.query(`select email from usr where user_id = $1`, [ws.user_ID]);
let taxes = pool.query(`select name, rate::text from store_tax where store_id = $1`, [parameters.store]);
let li = pool.query(`
select distinct on (product_id)
product_id,
variant_id,
menu_item_id as menu_item,
producer,
brand,
product.display_name as name,
type,
strain.display_name as strain,
portions,
variant.quantity::text as quantity,
flavor,
gram_equivalency::text,
thc,
cbd,
price::text,
line_item.quantity as howmany,
destination as image
from
line_item
inner join menu_item using (menu_item_id)
inner join variant using (variant_id)
inner join product using (product_id)
left outer join strain using (strain_id)
left outer join product_image using (product_id)
where cart_id = $1
`, [data.rows[0].cart_id])
ws.publish('store/reservations/' + parameters.store, JSON.stringify({
what: 'store/reservations/' + parameters.store,
how: 'add',
data: {
user_id: ws.user_ID,
user_email: (await email).rows[0].email,
statuses: [{status: 'placed', as_of: data.rows[0].system_time_start}],
taxes: (await taxes).rows,
line_items: (await li).rows
}
}));
break;
}
let data = await pool.query('select * from reservation($1)', [kartik]);
if(data.rowCount === 1) {
let out;
for(let j = 0; j < data.rows[0].statuses.length; ++j) {
if(j === data.rows[0].statuses.length - 1 && data.rows[0].statuses[j].end !== 'infinity' && (data.rows[0].statuses[j].status === 'placed' || data.rows[0].statuses[j].status === 'ready')) {
out = {status: 'store_cancelled', as_of: data.rows[0].statuses[j].end};
}
delete data.rows[0].statuses[j].end;
}
if(out) {
data.rows[0].statuses.push(out);
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'invalid cart'
}));
}
break;
}
case 'claim': {
let kartik;
try {
kartik = cryptids.s2i(parameters.cart);
} catch(e) {
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'invalid cart'
}));
break;
}
const auth = await is(ws);
let n = Number.isInteger(Number(parameters.user_ID));
if(auth === undefined) {
if(ws.user_ID) {
authenticated.delete(ws.user_ID);
ws.publish('user/authenticated', JSON.stringify({
what: 'user/authenticated',//tbd
how: 'replace',
data: authenticated.size
}));
delete ws.user_ID;
}
} else if(!auth && n) {
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'unauthorized'
}));//client side virtual event user god false
} else {
const id = auth && n ? parameters.user_ID : ws.user_ID;
let r = (await pool.query(`
update cart
set user_id = $1
where
cart_id = $2 and
user_id is null and
status > 'active'
`, [id, kartik]
));
if(r.rowCount === 1) {
ws.send(JSON.stringify({
response_ID: request_ID
}));//redirect to all of user's reservations
ws.publish('reservations/' + parameters.cart, JSON.stringify({
what: 'reservations/' + parameters.cart,
how: 'replace',
why: 'claimed'
}));
} else {
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'invalid cart'
}));
}
let r = (await pool.query(`update cart set status = 'user_cancelled' where user_id = $1 and store_id = $2 and (status = 'placed' or status = 'ready') returning system_time_start`, [id, parameters.store]));
let params = [id, kartik];
let precondition;
if(parameters.update === 'cancel') {
params.push('user_cancelled');
precondition = "(x.status = 'placed' or x.status = 'ready')";
} else if(parameters.update === 'reserve') {
params.push('placed');
precondition = "x.status = 'active'";
}
let data = 'invalid update';
if(params.length === 3) {
//https://stackoverflow.com/a/7927957
let r = await pool.query(`
update cart as x
set
status = $3,
user_id = case
when x.user_id is null then $1
else x.user_id
end
from (select cart_id, user_id from cart where cart_id = $2 for update) as y
where
x.cart_id = y.cart_id and
(x.user_id is null or x.user_id = $1) and
${precondition}
returning x.system_time_start, x.store_id, y.user_id as old_user`,
params
);
if(r.rowCount === 1) {
data = undefined;
if(parameters.update === 'cancel') {
ws.publish('store/reservations/' + r.rows[0].store_id, JSON.stringify({
what: 'store/reservations/' + r.rows[0].store_id,
how: 'update',
data: {
cart_id: parameters.cart,
status: 'user_cancelled',
as_of: r.rows[0].system_time_start
}
}));
if(id) {//https://github.com/uNetworking/uWebSockets.js/issues/362
ws.publish('user/reservations/' + id, JSON.stringify({
what: 'user/reservations/' + id,
how: 'update',
data: {
cart_id: parameters.cart,
status: 'user_cancelled',
as_of: r.rows[0].system_time_start
}
}));
if(r.rows[0].old_user === null) {
ws.publish('reservations/' + parameters.cart, JSON.stringify({
what: 'reservations/' + parameters.cart,
how: 'replace',
why: 'claimed'
}));
}
} else {
ws.publish('reservations/' + parameters.cart, JSON.stringify({
what: 'reservations/' + parameters.cart,
how: 'update',
data: {
status: 'user_cancelled',
as_of: r.rows[0].system_time_start
}
}));
}
} else {//'reserve'
//distinct on is because product_image may multiple number of rows
let line_items = (await pool.query(`
select distinct on (product_id)
product_id,
variant_id,
menu_item_id as menu_item,
producer,
brand,
product.display_name as name,
type,
strain.display_name as strain,
coalesce(product.species, strain.species) as species,
blend,
portions,
variant.quantity::text as quantity,
flavor,
gram_equivalency::text,
thc,
cbd,
price::text,
line_item.quantity as howmany,
destination as image
from
line_item
inner join menu_item using (menu_item_id)
inner join variant using (variant_id)
inner join product using (product_id)
left outer join strain using (strain_id)
left outer join product_image using (product_id)
where cart_id = $1
`, [kartik])).rows;
let types = new Set();
for(let i = 0; i < line_items.length; ++i) {
types.add(line_items[i].type);
}
let taxes = (await pool.query(`select name, rate::text, types from store_tax where store_id = $1 and (types is null or types && $2)`, [r.rows[0].store_id, [...types]])).rows;
ws.publish('store/reservations/' + r.rows[0].store_id, JSON.stringify({
what: 'store/reservations/' + r.rows[0].store_id,
how: 'add',
data: {
cart_id: parameters.cart,
statuses: [{status: 'placed', as_of: r.rows[0].system_time_start}],
taxes,
line_items
}
}));
if(id) {//https://github.com/uNetworking/uWebSockets.js/issues/362
ws.publish('user/reservations/' + id, JSON.stringify({
what: 'user/reservations/' + id,
how: 'add',
data: {
cart_id: parameters.cart,
statuses: [{status: 'placed', as_of: r.rows[0].system_time_start}],
taxes,
line_items
}
}));
}
}
}
}
if(Number.isInteger(Number(parameters?.store)) && Number.isInteger(Number(parameters?.user))) {
let out = [];
if(!Number.isInteger(Number(parameters.store))) {
out.push('store');
}
let kartik;
try {
kartik = cryptids.s2i(parameters.cart);
} catch(e) {
out.push('cart');
}
if(out.length) {
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'missing ' + out.join(' and ')
}));
} else {
data = (await pool.query(
`update cart set status = 'ready' where store_id = $1 and user_id = $2 and status = 'placed' returning system_time_start`,
[parameters.store, parameters.user]))
params.push('ready', 'placed');
case 'paid'://todo: decrement stock. maybe trigger?
data = (await pool.query(
`update cart set status = 'paid' where store_id = $1 and user_id = $2 and status = 'ready' returning system_time_start`,
[parameters.store, parameters.user]))
case 'paid':
params.push('paid', 'ready');
data = (await pool.query(
`update cart set status = 'store_cancelled' where store_id = $1 and user_id = $2 and status = 'placed' returning system_time_start`,
[parameters.store, parameters.user]))
params.push('store_cancelled', 'placed');
data = (await pool.query(
`update cart set status = 'no_show' where store_id = $1 and user_id = $2 and status = 'ready' returning system_time_start`,
[parameters.store, parameters.user]))
params.push('no_show', 'ready');
break;
if(data?.rowCount === 1) {
ws.send(JSON.stringify({
response_ID: request_ID
}));
if(parameters.update === 'cancel') {
parameters.update = 'store_cancelled';
} else if(parameters.update === 'noshow') {
parameters.update = 'no_show';
}
ws.publish('store/reservations/' + parameters.store, JSON.stringify({
what: 'store/reservations/' + parameters.store,
how: 'update',
data: {
user_id: id,
status: parameters.update,
as_of: data.rows[0].system_time_start
let data = 'invalid update';
if(params.length === 4) {
let query = await pool.query(
`update cart set status = $3 where store_id = $1 and cart_id = $2 and status = $4 returning system_time_start, user_id`,
params
);
if(query.rowCount === 1) {
data = undefined;
ws.publish('store/reservations/' + parameters.store, JSON.stringify({
what: 'store/reservations/' + parameters.store,
how: 'update',
data: {
cart_id: parameters.cart,
status: params[2],
as_of: query.rows[0].system_time_start
}
}));
if(query.rows[0].user_id === null) {
ws.publish('reservations/' + parameters.cart, JSON.stringify({
what: 'reservations/' + parameters.cart,
how: 'update',
data: {
status: params[2],
as_of: query.rows[0].system_time_start
}
}));
} else {
ws.publish('user/reservations/' + query.rows[0].user_id, JSON.stringify({
what: 'user/reservations/' + query.rows[0].user_id,
how: 'update',
data: {
cart_id: parameters.cart,
status: params[2],
as_of: query.rows[0].system_time_start
}
}));