XBHLXAKNBJNWSXZMHVKDMDVI7CUF5MN22FFS3XQIOLFDJHDCLTVAC
NHVZFKZDPQGCZ6UOVQU5U3Q3SOAKGOGZNFIE7C6IB6YP3ZS4VVDAC
RAN5QU4U2OVNLDGZVVTJNCXDN5GNAVPXGKHADZO2ML3Y5KLU4QPQC
QFATAUXF3SMSTZULKPCK4W3AZ4NEO3VFZE7SWIBDBXGC7JU5L6TAC
QQAJ5O5EW6ASWLXQ627Y2F6YGMYKCM4WS37LTZYOMNPPFDPMNBUAC
ZI2RJOZ2HXBHX7L54BTSDKESY6NG5KBJLVHCNY7K7ZLGIUOIHQUQC
E4HFJ4L4PAGV7R7EXVG2B2CWDGGW7XWU4D37VURZ66HZ3ILHCFUAC
PMEQGKI5R3WTMDKH4B2PN67XDRMYZFULPKPSDTOGF5BPDGYNO2KQC
JP2LJ6B34S4IS6MPKF5LMMYUWIYMGU3JCJRRR4PTZ4IWM653JCXAC
NDMM44EV2XD5RP7J4Q25ZX52LUP7WOMTKO3C2PDXW7IWFEVWUW6QC
XQA3IQXSGAO2FST3F64WLVZ7JNLMQYKT3LO4SHHKYR2D765OPGHQC
Y4W4MACPKH3IVO6JIQYGN4V5LR6EM6RPZEJ4RHH6ZBZUDA63AGRQC
VO5G3FF3NDOKCIF2OU7G257R3XIPTFDCAMDTOVKHKOB5Z3I55KDQC
SE6MCCXTIXSGMAH5EL7EPDXIQKV6THCCY2H4OQJJQOHCUQAWE5VQC
Y6DVC22CQACFO75XTUE4BZQXCVJAYOECY2F7M2IYV7AWFGOF5G3QC
7XVW32MMBBIGWQZ3QRD365TBIJU4DYN6T4VO2E46WFDCFHGO2BMAC
WYTMZJFYVKHR4QH7AV5JUNWXT6NAC5NNQNPZCQSDI6LGI7DVXFYAC
2ZHTBPOJQI4FS3OQJXUDBD62HVFPGSKI633IXXKAGPGQLURMKVPAC
63VXWIHIAKGK7J4VTNRUAG2V32N2QUSWFELB6GD34S54FGRWAPCQC
CMOE5TZ3HY6BG6KB56HMIJ6BU5J6D3NE7VFK2NSAX2XEXAPEKJQQC
4WREYORZT3SWUXADWHSS6B4PQSP3QSLH77CGNKRH6IRKUMX4TARAC
const postgres = new (require('pg').Client)();
const pg = require('pg');
const postgres = new pg.Client();
const googleMapsClient = new (require("@googlemaps/google-maps-services-js").Client)({});
const gay = /^Friendly Stranger/i;
const gay2 = /^stash\s+(and|&)\s+co/i;
const gay8 = /^fire\s+(and|&)\s+flower/i;
const gay3 = /^tokyo\s+smoke/i;
const gay4 = /^spiritleaf/i;
const gay5 = /^CANNACO/i;
const gay6 = /^one\s+plant/i;
const gay7 = /^sessions/i;
sanitize_name: function (n) {
if(n === 'Cannabis Boutique Inc.') {
n = 'Cannabis Boutique';
} else if(n === 'MARY JANE RIGS & CANNABIS') {
n = 'Mary Jane Rigs & Cannabis';
} else if(n === 'HELLO CANNABIS SSM') {
n = 'HELLO CANNABIS'
} else if(gay2.test(n)) {
n = 'Stash & Co.';
} else if(gay3.test(n)) {
n = 'Tokyo Smoke';
} else if(gay4.test(n)) {
n = 'SPIRITLEAF';
} else if(gay5.test(n)) {
n = 'CANNACO';
} else if(gay6.test(n)) {
n = 'One Plant';
} else if(gay7.test(n)) {
n = 'Sessions';
} else if(gay8.test(n)) {
n = 'Fire & Flower';
} else if(n.toLowerCase().trim() === 'rainbow thunder bay cannabis accessories') {
n = 'Rainbow';
} else if(n !== 'Made In Cannabis' && n !== 'Mary Jane on Penny Lane' && n !== 'This Is Cannabis' && n !== 'BURNSIDE BUDS.CA' && n !== 'The Cannabis Guys') {
n = n.replace(/\./g, '');
let first = n.split(/\s+/);
//if(first.length > 2) {
[first, ...n] = first;
n = n.join(' ');
//todo: replace if not prefixed by "of", "the", etc. what are those words called?
n = n.split('-')[0].split(/\bat\b/i)[0].split(/\bon\b/i)[0].replace(/\b(sale(s?)|shop|retail|boutique|market|premium|cannabis|store(s?)|corp(orat(ed|ion))?|inc(orporat(ed|ion))?|co(mpany)?|supply|retailer(s?)|limited|ltd|calgary|bridgeland|downtown|kensington|barrhaven|recreational|dispensary)\b|\(.+\)/gi,'').trim();
n = (first + ' ' + n).trim().replace(/\s+/g, ' ');
if(n === '420') {
//console.log('dumbass');
n = 'FOUR20';
} else if(n === 'HOBO') {
n = 'Hobo';
} else if(n === 'NUMO') {
n = 'Numo';
} else if(gay.test(n)) {
n = "Friendly Stranger";
}
//}
}
if(n.toUpperCase() === n) {//todo: check how many are title cased, all caps, all lower.
//console.log(`don't yell at me :( ${n}`);
}
return n;
},
g: async function (address) {
let addr = {address: '', address2: ''};
let location = (await googleMapsClient.geocode({params: {address, key: process.env.GAPI}})).data;
if(location.status === 'OK') {
let wow = 0;
for(let j = 0; j < location.results[0].address_components.length; ++j) {
if(location.results[0].address_components[j].types.includes('administrative_area_level_1')) {
addr.region = location.results[0].address_components[j].long_name;
++wow;
} else if(location.results[0].address_components[j].types.includes('country')) {
addr.country = location.results[0].address_components[j].long_name;
++wow;
} else if(location.results[0].address_components[j].types.includes('locality')) {
addr.city = location.results[0].address_components[j].long_name;
++wow;
} else if(location.results[0].address_components[j].types.includes('postal_code')) {
addr.postal_code = location.results[0].address_components[j].long_name.replace(/\s+/, '');
++wow;
} else if(location.results[0].address_components[j].types.includes('route')) {
addr.address += ' ' + location.results[0].address_components[j].long_name;
++wow;
} else if(location.results[0].address_components[j].types.includes('street_number')) {
addr.address += location.results[0].address_components[j].long_name;
++wow;
} else if(location.results[0].address_components[j].types.includes('subpremise')) {
addr.address2 = location.results[0].address_components[j].long_name;
}
}
if(wow < 6) {
console.log('hardcode me', address, location);
throw('unparseable address');
}
location = location.results[0].geometry.location;
let [tz, foot, car] = await Promise.all([
googleMapsClient.timezone({params: {location, timestamp: 0, key: process.env.GAPI}}),
request(`http://127.0.0.1:5000/nearest/v1/fuck/${location.lng},${location.lat}`).then(x => JSON.parse(x).waypoints[0].hint),
request(`http://127.0.0.1:5001/nearest/v1/fuck/${location.lng},${location.lat}`).then(x => JSON.parse(x).waypoints[0].hint)
]);
if(tz.data.status === 'OK') {
return [addr, location, tz.data.timeZoneId, foot, car];
} else {
throw tz;
}
} else {
throw location;
}
},
const request = require('request-promise-native');
require('dotenv').config({path: '../.env'});
const { Client } = require('pg');
let postgres = new Client();
Object.entries(require('./common.js')).forEach(([name, exported]) => global[name] = exported);
let hints = Promise.all([
request('http://127.0.0.1:5000/nearest/v1/fuck/-120.2209884,55.7501513').then(x => JSON.parse(x).waypoints[0].hint),
request('http://127.0.0.1:5001/nearest/v1/fuck/-120.2209884,55.7501513').then(x => JSON.parse(x).waypoints[0].hint),
'https://images.squarespace-cdn.com/content/5d2516d4eafe0d00016d4f52/1563851764355-Z79A7Y9YMODUHQ06GDR2'
]);
.then(async () => postgres.query(
`with storeid as (
select ios_store_id(
'Dawson Creek Cannabis Co.',
'british columbia',
'canada',
'11000 8th Street',
'Unit 19',
.then(async () => {
let r = await g("UNIT 19 - 11000 8 St, Dawson Creek, V1G 4K6, british columbia, canada");
return postgres.query(
`select ios_store_id(
)
),
ins as (
insert into store_image (store_id, url)
select ios_store_id, $3 from storeid
on conflict do nothing
)
select ios_store_id from storeid`,
await hints
));
)`,
[r[0].region, r[0].country, r[0].address, r[0].address2, r[0].city, r[0].postal_code, r[1].lng, r[1].lat, r[3], r[4], r[2]]
);
});
//todo: other official government online stores, prescedence to be deterministic, canada only?
//todo: other official government online stores
//default concentrations from online stores with fixed prescedence. OCS seems to have the highest quality data
name = 'Ontario Cannabis Store' and region = 'ontario' and country = 'canada' or
name = 'BC Cannabis Stores' and region = 'british columbia' and country = 'canada'
joined_name = 'ocs' and region = 'ontario' or
joined_name = 'bc' and region = 'british columbia' or
joined_name = 'alberta' and region = 'alberta'
function sanitize_name(n) {
if(n === 'Cannabis Boutique Inc.') {
n = 'Cannabis Boutique';
} else if(n === 'MARY JANE RIGS & CANNABIS') {
n = 'Mary Jane Rigs & Cannabis';
} else if(n === 'HELLO CANNABIS SSM') {
n = 'HELLO CANNABIS'
} else if(gay2.test(n)) {
n = 'Stash & Co.';
} else if(gay3.test(n)) {
n = 'Tokyo Smoke';
} else if(gay4.test(n)) {
n = 'SPIRITLEAF';
} else if(gay5.test(n)) {
n = 'CANNACO';
} else if(gay6.test(n)) {
n = 'One Plant';
} else if(gay7.test(n)) {
n = 'Sessions';
} else if(gay8.test(n)) {
n = 'Fire & Flower';
} else if(n.toLowerCase().trim() === 'rainbow thunder bay cannabis accessories') {
n = 'Rainbow';
} else if(n !== 'Made In Cannabis' && n !== 'Mary Jane on Penny Lane' && n !== 'This Is Cannabis' && n !== 'BURNSIDE BUDS.CA' && n !== 'The Cannabis Guys') {
n = n.replace(/\./g, '');
let first = n.split(/\s+/);
//if(first.length > 2) {
[first, ...n] = first;
n = n.join(' ');
//todo: replace if not prefixed by "of", "the", etc. what are those words called?
n = n.split('-')[0].split(/\bat\b/i)[0].split(/\bon\b/i)[0].replace(/\b(sale(s?)|shop|retail|boutique|market|premium|cannabis|store(s?)|corp(orat(ed|ion))?|inc(orporat(ed|ion))?|co(mpany)?|supply|retailer(s?)|limited|ltd|calgary|bridgeland|downtown|kensington|barrhaven|recreational|dispensary)\b|\(.+\)/gi,'').trim();
n = (first + ' ' + n).trim().replace(/\s+/g, ' ');
if(n === '420') {
//console.log('dumbass');
n = 'FOUR20';
} else if(n === 'HOBO') {
n = 'Hobo';
} else if(n === 'NUMO') {
n = 'Numo';
} else if(gay.test(n)) {
n = "Friendly Stranger";
}
//}
}
if(n.toUpperCase() === n) {//todo: check how many are title cased, all caps, all lower.
//console.log(`don't yell at me :( ${n}`);
}
return n;
}
async function g(address) {
let addr = {address: '', address2: ''};
let location = (await googleMapsClient.geocode({params: {address, key: process.env.GAPI}})).data;
if(location.status === 'OK') {
let wow = 0;
for(let j = 0; j < location.results[0].address_components.length; ++j) {
if(location.results[0].address_components[j].types.includes('administrative_area_level_1')) {
addr.region = location.results[0].address_components[j].long_name;
++wow;
} else if(location.results[0].address_components[j].types.includes('country')) {
addr.country = location.results[0].address_components[j].long_name;
++wow;
} else if(location.results[0].address_components[j].types.includes('locality')) {
addr.city = location.results[0].address_components[j].long_name;
++wow;
} else if(location.results[0].address_components[j].types.includes('postal_code')) {
addr.postal_code = location.results[0].address_components[j].long_name.replace(/\s+/, '');
++wow;
} else if(location.results[0].address_components[j].types.includes('route')) {
addr.address += ' ' + location.results[0].address_components[j].long_name;
++wow;
} else if(location.results[0].address_components[j].types.includes('street_number')) {
addr.address += location.results[0].address_components[j].long_name;
++wow;
} else if(location.results[0].address_components[j].types.includes('subpremise')) {
addr.address2 = location.results[0].address_components[j].long_name;
}
}
if(wow < 6) {
console.log('hardcode me', address, location);
throw('unparseable address');
}
location = location.results[0].geometry.location;
let [tz, foot, car] = await Promise.all([
googleMapsClient.timezone({params: {location, timestamp: 0, key: process.env.GAPI}}),
request(`http://127.0.0.1:5000/nearest/v1/fuck/${location.lng},${location.lat}`).then(x => JSON.parse(x).waypoints[0].hint),
request(`http://127.0.0.1:5001/nearest/v1/fuck/${location.lng},${location.lat}`).then(x => JSON.parse(x).waypoints[0].hint)
]);
if(tz.data.status === 'OK') {
return [addr, location, tz.data.timeZoneId, foot, car];
} else {
throw tz;
}
} else {
throw location;
}
}
if(notmissing(parameters, ['name', 'address', 'city', 'region', 'country', 'postal_code'], ws, request_ID)) {
let params = [
parameters.name,
parameters.URL,
parameters.address,
parameters.address2 ? parameters.address2 : null,
parameters.city,
parameters.region,
parameters.country,
parameters.postal_code,
Boolean(parameters.delivery),
Boolean(parameters.pickup),
Boolean(parameters.prepayment),
parameters.phone ? parameters.phone : null
];
//I went through the effort of delaying gapi as long as I could and folding three inserts into one transaction, only for the unique constraint to goof me up. Likely won't happen often though, so accept the tradeoff.
let q = 1;
let query = '';
if(Array.isArray(parameters.taxes) && parameters.taxes.length) {
query += `, insert${++q} as (
insert into store_tax (store_id, name, rate, types)
values ${parameters.taxes.map(tax => `((select store_id from insert1), $${params.push(tax.name)}, $${params.push(tax.rate)}, $${params.push(tax.types)})`).join(', ')}
)`;
let addr, location, tz, foot, car;
try {
[addr, location, tz, foot, car] = await g(`${parameters.address}, ${parameters.address2}, ${parameters.city}, ${parameters.region}, ${parameters.country}, ${parameters.postal_code}`);
} catch(e) {
if(e.response) {
console.error(e.response.data);
} else {
console.error(e);
if(Array.isArray(parameters.times)) {
let fuck = [];
for(let i = 0; i < parameters.times.length; ++i) {
try {
let ts = parameters.times[i].time_start.split(':');
ts = Number(parameters.times[i].day_start) * 1440 + Number(ts[0]) * 60 + Number(ts[1]);
let te = parameters.times[i].time_end.split(':');
te = Number(parameters.times[i].day_end) * 1440 + Number(te[0]) * 60 + Number(te[1]);
if(te <= ts) {
fuck.push(`(int4range($${params.push(ts)}, 10080)),(int4range(0, $${params.push(te)}))`);
//oops possibly 0,0 empty range. nvm good job range_agg
} else {
fuck.push(`(int4range($${params.push(ts)}, $${params.push(te)}))`);
}
} catch(e) {}
}
if(fuck.length) {
query += `, insert${++q} as (
insert into store_time (store_id, open)
select store_id, unnest from insert1 cross join (select unnest(range_agg(column1, true, true)) from (values ${fuck.join(',')}) as ex) as idc
)`;
}
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'geocode failure'
}));
break;
}
let fuck = [];
if(Array.isArray(parameters.times)) {
for(let i = 0; i < parameters.times.length; ++i) {
try {
let ts = parameters.times[i].time_start.split(':');
ts = Number(parameters.times[i].day_start) * 1440 + Number(ts[0]) * 60 + Number(ts[1]);
let te = parameters.times[i].time_end.split(':');
te = Number(parameters.times[i].day_end) * 1440 + Number(te[0]) * 60 + Number(te[1]);
if(ts < 0 || ts >= 10080 || te < 0 || te >= 10080) {
continue;
}
if(te <= ts) {
fuck.push([ts, 10080], [0, te]);
//oops possibly 0,0 empty range. nvm good job range_agg
} else {
fuck.push([ts, te]);
}
} catch(e) {}
if(Array.isArray(parameters.images) && parameters.images.length) {
query += `, insert${++q} as (
insert into store_image (store_id, URL)
values ${parameters.images.map(image => `((select store_id from insert1), $${params.push(image)})`).join(', ')}
)`;
}
/*
two google maps api error handlers because their api is inconsistent as fuck
missing api key will return 'REQUEST_DENIED', but 200, so (2)
wrong parameters will return 'INVALID_REQUEST', so 400 (1) for geocode
but 200 (2) for timezone!! 400 throws automatically, but 200 need to be thrown manually
}
let jn = sanitize_name(parameters.name)
let params = [
jn,
jn,
parameters.URL,
addr.address,
addr.address2,
addr.city,
addr.region,
addr.country,
addr.postal_code,
location.lng,
location.lat,
foot,
car,
tz,
parameters.phone ? parameters.phone : null,
parameters.phone && parameters.extension ? parameters.extension : null,
Boolean(parameters.delivery),
Boolean(parameters.pickup),
Boolean(parameters.prepayment),
JSON.stringify(fuck)
];
let q = 1;
let query = '';
if(Array.isArray(parameters.taxes) && parameters.taxes.length) {
query += `, insert${++q} as (
insert into store_tax (store_id, name, rate, types)
values ${parameters.taxes.map(tax => `((select store_id from insert1), $${params.push(tax.name)}, $${params.push(tax.rate)}, $${params.push(tax.types)})`).join(', ')}
)`;
}
if(Array.isArray(parameters.images) && parameters.images.length) {
query += `, insert${++q} as (
insert into store_image (store_id, URL)
values ${parameters.images.map(image => `((select store_id from insert1), $${params.push(image)})`).join(', ')}
)`;
}
pool.query(
`with insert1 as (
insert into store (
joined_name,
name,
url,
address,
address2,
city,
region,
country,
postal_code,
longitude,
latitude,
osrm_hint_foot,
osrm_hint_car,
timezone,
phone,
extension,
delivery,
pickup,
prepayment,
open
) select
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14,
$15,
$16,
$17,
$18,
$19,
(select range_agg(int4range((x->>0)::int, (x->>1)::int), true, true) from json_array_elements($20) as t(x))
returning store_id
)${query}
select * from get_stores((select store_id from insert1))`,
params
).then(r => {
ws.publish('store', JSON.stringify({
what: 'store',
how: 'add',
data: r.rows[0]
}));
ws.send(JSON.stringify({
response_ID: request_ID
}));
}).catch(e => {
/* todo
Class 23 https://www.postgresql.org/docs/current/errcodes-appendix.html with e.code
e.table
https://stackoverflow.com/a/4108266
use implicit names in initialize.sql. then, can use e.constraint, remove e.table + _ from beginning, remove suffix to see which columns are affected
e.detail is fine for 23505, but 23514 just complains about the row as a whole. how can we get the specific details? https://stackoverflow.com/a/47972916 for python...
useful
code: '23505',
detail: 'Key (store, name)=(21, hst) already exists.',
useless
[message]: 'new row for relation "store_tax" violates check constraint "store_tax_rate_check"',
name: 'error',
length: 266,
severity: 'ERROR',
code: '23514',
detail: 'Failing row contains (15, 28, hst, 2, 2020-02-11 20:20:54.923496-05, infinity).',
what I want: rate > 0 and rate < 1
2 > 0 and 2 < 1
true and false
false
let location;
try {
location = (await googleMapsClient.geocode({params: {address: `${parameters.address}, ${parameters.address2}, ${parameters.city}, ${parameters.region}, ${parameters.country}, ${parameters.postal_code}`, key: process.env.GAPI}})).data;
if(location.status === 'OK') {
location = location.results[0].geometry.location;
} else {
throw location;
}
} catch(e) {
if(e.response) {
console.error(e.response.data);//google maps api(1)
}
else if(e.status) {
console.error(e);//google maps api(2)
}
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'google geocode failure'
}));//todo: status: 'ZERO_RESULTS' => "invalid address :("
break;
}
let hints = Promise.all([
request(`http://127.0.0.1:5000/nearest/v1/fuck/${location.lng},${location.lat}`).then(x => JSON.parse(x).waypoints[0].hint),
request(`http://127.0.0.1:5001/nearest/v1/fuck/${location.lng},${location.lat}`).then(x => JSON.parse(x).waypoints[0].hint)
]);
let tz;
try {
tz = (await googleMapsClient.timezone({params: {location, timestamp: 0, key: process.env.GAPI}})).data;
if(tz.status === 'OK') {
tz = tz.timeZoneId;
} else {
throw tz;
}
} catch(e) {
if(e.response) {
console.error(e.response.data);//google maps api(1)
}
else if(e.status) {
console.error(e);//google maps api(2)
}
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'google timezone failure'
}));
break;
}
hints = await hints;
//promise then catch is fine since this is the last thing to do this function. don't need to break;
pool.query(
`with insert1 as (
insert into store (
name,
url,
address,
address2,
city,
region,
country,
postal_code,
delivery,
pickup,
prepayment,
phone,
longitude,
latitude,
osrm_hint_foot,
osrm_hint_car,
timezone
) values (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$${params.push(location.lng)},
$${params.push(location.lat)},
$${params.push(hints[0])},
$${params.push(hints[1])},
$${params.push(tz)}
)
returning store_id
)${query}
select * from get_stores((select store_id from insert1))`,
params
).then(r => {
ws.publish('store', JSON.stringify({
what: 'store',
how: 'add',
data: r.rows[0]
}));
ws.send(JSON.stringify({
response_ID: request_ID
}));
}).catch(e => {
/* todo
Class 23 https://www.postgresql.org/docs/current/errcodes-appendix.html with e.code
e.table
https://stackoverflow.com/a/4108266
use implicit names in initialize.sql. then, can use e.constraint, remove e.table + _ from beginning, remove suffix to see which columns are affected
e.detail is fine for 23505, but 23514 just complains about the row as a whole. how can we get the specific details? https://stackoverflow.com/a/47972916 for python...
useful
code: '23505',
detail: 'Key (store, name)=(21, hst) already exists.',
useless
[message]: 'new row for relation "store_tax" violates check constraint "store_tax_rate_check"',
name: 'error',
length: 266,
severity: 'ERROR',
code: '23514',
detail: 'Failing row contains (15, 28, hst, 2, 2020-02-11 20:20:54.923496-05, infinity).',
what I want: rate > 0 and rate < 1
2 > 0 and 2 < 1
true and false
false
*/
console.log(e);
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'db fuckup'
}));
});
}
console.log(e);
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'db fuckup'
}));
});