simplify and automate offer creation

This commit is contained in:
ntr committed 2022-09-02 00:06:51 +10:00
1 parent 7ea2c37116
commit a43d4f32e4
10 files changed
+97 -67

No files matched your search

+7 -1
View File
@@ -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
View File
@@ -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();
} }
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+2 -1
View File
@@ -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
View File
@@ -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: