simplify and automate offer creation
This commit is contained in:
1 parent
7ea2c37116
commit
a43d4f32e4
10 files changed
+97
-67
No files matched your search
@@ -1,3 +1,9 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
get mcrates.au up
|
push offers
|
||||||
|
redux that bitch
|
||||||
|
|
||||||
|
|
||||||
|
seller
|
||||||
|
eg min 6 items
|
||||||
|
shipping is 101aud
|
||||||
+2
-1
@@ -28,7 +28,8 @@ async function start() {
|
|||||||
events,
|
events,
|
||||||
} = createServices(log);
|
} = createServices(log);
|
||||||
|
|
||||||
events.emit(Events.StartupSync);
|
// events.emit(Events.StartupSync);
|
||||||
|
events.emit(Events.ListingsUpdated);
|
||||||
|
|
||||||
await api.listen();
|
await api.listen();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ model Offer {
|
|||||||
want Want @relation(fields: [wantId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
want Want @relation(fields: [wantId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
// only one offer per want for a user
|
// only one offer per want for a user
|
||||||
@@unique([userId, wantId])
|
// @@unique([userId, wantId])
|
||||||
|
@@unique([listingId])
|
||||||
@@map("offers")
|
@@map("offers")
|
||||||
}
|
}
|
||||||
+1
-41
@@ -37,46 +37,6 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
|||||||
|
|
||||||
const router = koaRouter();
|
const router = koaRouter();
|
||||||
|
|
||||||
interface ListingData extends Listing {
|
|
||||||
data: any,
|
|
||||||
want: { user: User },
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getBundles() {
|
|
||||||
const bundles = await pg.bundleCandidates();
|
|
||||||
|
|
||||||
return bundles.map((b: ListingData[]) => {
|
|
||||||
const total = b.reduce((acc, l) => acc + l.data.price.value, 0);
|
|
||||||
const items = b.map(l => ({
|
|
||||||
id: l.data.id,
|
|
||||||
description: l.data.release.description,
|
|
||||||
originalPrice: l.data.original_price.value,
|
|
||||||
originalCurrency: l.data.original_price.curr_abbr,
|
|
||||||
price: l.data.price.value,
|
|
||||||
currency: l.data.price.currency,
|
|
||||||
condition: l.data.condition,
|
|
||||||
uri: l.data.uri,
|
|
||||||
user: l.want.user,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const shipping = b[0]?.data.seller.shipping;
|
|
||||||
|
|
||||||
const bundle = {
|
|
||||||
username: b[0]?.data.seller.username,
|
|
||||||
seller: b[0]?.data.seller.id,
|
|
||||||
shipsFrom: b[0]?.data.ships_from,
|
|
||||||
total,
|
|
||||||
currency: b[0]?.data.price.currency,
|
|
||||||
shipping: '...',
|
|
||||||
items,
|
|
||||||
mentionsAustralia: shipping.toLowerCase().includes('australia'),
|
|
||||||
mentionsWorldwide: shipping.toLowerCase().includes('world'),
|
|
||||||
}
|
|
||||||
|
|
||||||
return bundle;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function listen() {
|
function listen() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
app.on('error', err => {
|
app.on('error', err => {
|
||||||
@@ -105,7 +65,7 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get('/acp/bundles', async function bundlesGet(ctx, next) {
|
router.get('/acp/bundles', async function bundlesGet(ctx, next) {
|
||||||
const bundles = await getBundles();
|
const bundles = await offers.bundles();
|
||||||
ctx.response.body = bundles;
|
ctx.response.body = bundles;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+28
-1
@@ -11,9 +11,10 @@ import routerAuth from './routers/router.auth';
|
|||||||
export enum Events {
|
export enum Events {
|
||||||
WantsAdded = 'WantsAdded',
|
WantsAdded = 'WantsAdded',
|
||||||
StartupSync = 'StartupSync',
|
StartupSync = 'StartupSync',
|
||||||
|
ListingsUpdated = 'ListingsUpdated',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createEvents(log: Log, pg: Pg, discogs) {
|
export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
||||||
const events = new EventEmitter({ captureRejections: true });
|
const events = new EventEmitter({ captureRejections: true });
|
||||||
|
|
||||||
events.on('error', function eventError(e) {
|
events.on('error', function eventError(e) {
|
||||||
@@ -50,5 +51,31 @@ export default function createEvents(log: Log, pg: Pg, discogs) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
events.on(Events.ListingsUpdated, async function listingsUpdated() {
|
||||||
|
const bundles = await offers.bundles();
|
||||||
|
log.info({ event: Events.ListingsUpdated, bundles: bundles.length });
|
||||||
|
|
||||||
|
// const bundle = {
|
||||||
|
// username: b[0]?.data.seller.username,
|
||||||
|
// seller: b[0]?.data.seller.id,
|
||||||
|
// shipsFrom: b[0]?.data.ships_from,
|
||||||
|
// total,
|
||||||
|
// currency: b[0]?.data.price.currency,
|
||||||
|
// shipping: '...',
|
||||||
|
// items,
|
||||||
|
// mentionsAustralia: shipping.toLowerCase().includes('australia'),
|
||||||
|
// mentionsWorldwide: shipping.toLowerCase().includes('world'),
|
||||||
|
// }
|
||||||
|
|
||||||
|
for (const bundle of bundles) {
|
||||||
|
log.info({ seller: bundle.seller, items: bundle.items.length }, 'generating offers...');
|
||||||
|
const listingIds = bundle.items.map(i => i.id);
|
||||||
|
const updatedOffers = await offers.generate(listingIds, 100, 30)
|
||||||
|
log.info({ offers: updatedOffers.length }, 'offers set');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('offers up to date');
|
||||||
|
});
|
||||||
|
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
+46
-2
@@ -19,9 +19,11 @@ export default function createOffers(log: Log, pg: Pg) {
|
|||||||
log.info('generateOffers', { listingIds, bulkShipping, baseShipping });
|
log.info('generateOffers', { listingIds, bulkShipping, baseShipping });
|
||||||
const listings = await pg.listingByIds(listingIds);
|
const listings = await pg.listingByIds(listingIds);
|
||||||
|
|
||||||
log.info({ listings }, 'creating offers for listings')
|
// log.info({ listings }, 'creating offers for listings')
|
||||||
|
|
||||||
const shipping = bulkShipping / listings.length;
|
const shipping = 10 + Math.random(); // yolt
|
||||||
|
|
||||||
|
// const shipping = bulkShipping / listings.length;
|
||||||
// how to deal with users who don't want all items in a bundle...
|
// how to deal with users who don't want all items in a bundle...
|
||||||
|
|
||||||
const offers = listings.map(l => ({
|
const offers = listings.map(l => ({
|
||||||
@@ -60,8 +62,50 @@ export default function createOffers(log: Log, pg: Pg) {
|
|||||||
return pg.offerSetStatus(offer.id, Status.Accepted);
|
return pg.offerSetStatus(offer.id, Status.Accepted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function bundles() {
|
||||||
|
const bundles = await pg.bundleCandidates();
|
||||||
|
|
||||||
|
interface ListingData extends Listing {
|
||||||
|
data: any,
|
||||||
|
want: { user: User },
|
||||||
|
};
|
||||||
|
|
||||||
|
return bundles.map((b: ListingData[]) => {
|
||||||
|
const total = b.reduce((acc, l) => acc + l.data.price.value, 0);
|
||||||
|
const items = b.map(l => ({
|
||||||
|
id: l.data.id,
|
||||||
|
description: l.data.release.description,
|
||||||
|
originalPrice: l.data.original_price.value,
|
||||||
|
originalCurrency: l.data.original_price.curr_abbr,
|
||||||
|
price: l.data.price.value,
|
||||||
|
currency: l.data.price.currency,
|
||||||
|
condition: l.data.condition,
|
||||||
|
uri: l.data.uri,
|
||||||
|
user: l.want.user,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const shipping = b[0]?.data.seller.shipping;
|
||||||
|
|
||||||
|
const bundle = {
|
||||||
|
username: b[0]?.data.seller.username,
|
||||||
|
seller: b[0]?.data.seller.id,
|
||||||
|
shipsFrom: b[0]?.data.ships_from,
|
||||||
|
total,
|
||||||
|
currency: b[0]?.data.price.currency,
|
||||||
|
shipping: '...',
|
||||||
|
items,
|
||||||
|
mentionsAustralia: shipping.toLowerCase().includes('australia'),
|
||||||
|
mentionsWorldwide: shipping.toLowerCase().includes('world'),
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundle;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generate,
|
generate,
|
||||||
accept,
|
accept,
|
||||||
|
bundles,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+6
-6
@@ -2,6 +2,7 @@ import bcrypt from 'bcrypt';
|
|||||||
import type Log from 'bunyan';
|
import type Log from 'bunyan';
|
||||||
import type { PrismaClient, Want, User, Listing, Offer, Prisma } from '@prisma/client';
|
import type { PrismaClient, Want, User, Listing, Offer, Prisma } from '@prisma/client';
|
||||||
import * as Bluebird from 'bluebird';
|
import * as Bluebird from 'bluebird';
|
||||||
|
import omit from 'lodash/omit';
|
||||||
|
|
||||||
import { Status } from './offers';
|
import { Status } from './offers';
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
status: 'desc',
|
total: 'asc',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -184,6 +185,8 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
seller, count(distinct("wantId")), array_agg(id) as listings
|
seller, count(distinct("wantId")), array_agg(id) as listings
|
||||||
from
|
from
|
||||||
listings
|
listings
|
||||||
|
where
|
||||||
|
seller not in (3709203)
|
||||||
group by
|
group by
|
||||||
seller
|
seller
|
||||||
order by
|
order by
|
||||||
@@ -208,13 +211,10 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
return await prisma.$transaction(
|
return await prisma.$transaction(
|
||||||
offers.map(offer => prisma.offer.upsert({
|
offers.map(offer => prisma.offer.upsert({
|
||||||
where: {
|
where: {
|
||||||
userId_wantId: {
|
listingId: offer.listingId,
|
||||||
userId: offer.userId,
|
|
||||||
wantId: offer.wantId,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
create: offer,
|
create: offer,
|
||||||
update: offer
|
update: omit(offer, ['status']),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -11,8 +11,8 @@ export default function creates(log) {
|
|||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
const pg = createPg(log, prisma);
|
const pg = createPg(log, prisma);
|
||||||
const discogs = createDiscogs(log, pg);
|
const discogs = createDiscogs(log, pg);
|
||||||
const events = createEvents(log, pg, discogs);
|
|
||||||
const offers = createOffers(log, pg);
|
const offers = createOffers(log, pg);
|
||||||
|
const events = createEvents(log, pg, discogs, offers);
|
||||||
const api = createApi(log, pg, discogs, offers, events);
|
const api = createApi(log, pg, discogs, offers, events);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export default function Offer(offer) {
|
|||||||
<h2>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank"><ExternalLink /></a></h2>
|
<h2>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank"><ExternalLink /></a></h2>
|
||||||
<h5 class='alt-text' >{details.year}</h5>
|
<h5 class='alt-text' >{details.year}</h5>
|
||||||
<h5 class='alt-text'>{details.formats[0].descriptions.join(' ')}</h5>
|
<h5 class='alt-text'>{details.formats[0].descriptions.join(' ')}</h5>
|
||||||
<h5 class='number'>${offer.total}</h5>
|
<h5 class='number'>mcrates price: ${offer.total}</h5>
|
||||||
|
<h5 class='number'>shipped by itself: ${offer.comparison}</h5>
|
||||||
</section>
|
</section>
|
||||||
<img src={details.thumb} />
|
<img src={details.thumb} />
|
||||||
<button onClick={onClick} disabled={accepted} class={classes} >{btnText}</button>
|
<button onClick={onClick} disabled={accepted} class={classes} >{btnText}</button>
|
||||||
|
|||||||
+2
-12
@@ -12,14 +12,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- "./db:/var/lib/postgresql/data"
|
- "./db:/var/lib/postgresql/data"
|
||||||
|
|
||||||
# pg-test:
|
|
||||||
# image: postgres:latest
|
|
||||||
# environment:
|
|
||||||
# POSTGRES_USER: 'mcrates'
|
|
||||||
# POSTGRES_DB: 'test'# POSTGRES_PASSWORD: 'digdeep'
|
|
||||||
# ports:
|
|
||||||
# - '55432:5432'
|
|
||||||
|
|
||||||
api:
|
api:
|
||||||
image: node:16
|
image: node:16
|
||||||
working_dir: /opt/mcrates/api
|
working_dir: /opt/mcrates/api
|
||||||
@@ -28,8 +20,6 @@ services:
|
|||||||
entrypoint: npm start
|
entrypoint: npm start
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: 'postgresql://mcrates:digdeep@pg:5432/mcrates?schema=public'
|
DATABASE_URL: 'postgresql://mcrates:digdeep@pg:5432/mcrates?schema=public'
|
||||||
ports:
|
|
||||||
- '41337:41337'
|
|
||||||
volumes:
|
volumes:
|
||||||
- "./api:/opt/mcrates/api"
|
- "./api:/opt/mcrates/api"
|
||||||
|
|
||||||
@@ -37,13 +27,13 @@ services:
|
|||||||
image: node:16
|
image: node:16
|
||||||
working_dir: /opt/mcrates/client
|
working_dir: /opt/mcrates/client
|
||||||
entrypoint: npm start
|
entrypoint: npm start
|
||||||
ports:
|
|
||||||
- '8080:8080'
|
|
||||||
volumes:
|
volumes:
|
||||||
- "./client:/opt/mcrates/client"
|
- "./client:/opt/mcrates/client"
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
|
depends_on:
|
||||||
|
- 'api'
|
||||||
volumes:
|
volumes:
|
||||||
- './nginx/localhost.nginx.conf:/etc/nginx/conf.d/default.conf'
|
- './nginx/localhost.nginx.conf:/etc/nginx/conf.d/default.conf'
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
Reference in new issue
Block a user