offers wip

This commit is contained in:
ntr committed 2022-08-23 00:02:50 +10:00
1 parent 453c2da045
commit deb9eb3fc2
16 files changed
+276 -88

No files matched your search

+10
View File
@@ -5,6 +5,16 @@ import { Events } from './src/events';
async function start() {
const log = createLog();
process.on('uncaughtException', (err, origin) => {
log.error({ err, origin }, 'uncaughtException');
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
log.error({ reason, promise }, 'unhandledRejection');
process.exit(1);
});
process.on('SIGTERM', () => {
log.debug('SIGTERM received, exiting');
process.exit(0);
+30
View File
@@ -16,6 +16,8 @@ model Listing {
wantId String @db.Uuid
want Want @relation(fields: [wantId], references: [id], onDelete: Cascade, onUpdate: Cascade)
offers Offer[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -32,6 +34,7 @@ model User {
updatedAt DateTime @updatedAt
wants Want[]
offers Offer[]
@@map("users")
}
@@ -48,9 +51,36 @@ model Want {
updatedAt DateTime @updatedAt
listings Listing[]
offers Offer[]
@@unique([userId, release])
@@index([release])
@@map("wants")
}
model Offer {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
status String
item Decimal @db.Money
shipping Decimal @db.Money
total Decimal @db.Money
comparison Decimal @db.Money
sellerId Int
listingId Int
listing Listing @relation(fields: [listingId], references: [id])
userId String @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
wantId String @db.Uuid
want Want @relation(fields: [wantId], references: [id], onDelete: Cascade, onUpdate: Cascade)
// only one offer per want for a user
@@unique([userId, wantId])
@@map("offers")
}
+35 -24
View File
@@ -5,10 +5,13 @@ import createServices from './services';
import createLog from './log';
const log = createLog();
log.level('error');
log.level('debug');
const services = createServices(log);
const HOST = 'http://localhost:41337';
const api = axios.create({
baseURL: 'http://localhost:41337',
withCredentials: true
});
before(function setup() {
services.api.listen();
@@ -19,29 +22,37 @@ after(function teardown(done) {
});
describe('api', function apiTests() {
it('listens', async function listenTest() {
const res = await axios.get(`${HOST}/health`);
assert.equal(res.status, 200);
describe.skip('auth', function authTests() {
it('listens', async function listenTest() {
const res = await api.get('/health');
assert.equal(res.status, 200);
});
it('register', async function register() {
const body = {
email: `test+${Date.now()}@user.com`,
password: 'woaaah theres no limit',
};
const { data } = await api.post('/auth/register', body);
assert.equal(data.email, body.email);
});
it('login', async function login() {
const body = {
email: `test+${Date.now()}@user.com`,
password: 'woaaah theres no limit',
};
await api.post('/auth/register', body);
const res = await api.post('/auth/login', body);
assert(res.headers['set-cookie']);
})
});
it('register', async function register() {
const body = {
email: `test+${Date.now()}@user.com`,
password: 'woaaah theres no limit',
};
const { data } = await axios.post(`${HOST}/auth/register`, body);
assert.equal(data.email, body.email);
describe('offers', function offerTests() {
it('create offers', async function createOffersTest() {
const { data: bundles } = await api.get('/bundles');
const { seller } = bundles[0];
const { data } = await api.post('/offers', { seller, shipping: 50.0, baseShipping: 15.0 });
});
});
it('login', async function login() {
const body = {
email: `test+${Date.now()}@user.com`,
password: 'woaaah theres no limit',
};
await axios.post(`${HOST}/auth/register`, body);
const res = await axios.post(`${HOST}/auth/login`, body);
assert(res.headers['set-cookie']);
})
});
+14 -14
View File
@@ -16,7 +16,7 @@ import { Pg } from './pg';
import routerAuth from './routers/router.auth';
import { Events } from './events';
export default function createApi(log: Log, pg: Pg, discogs, events) {
export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
const PORT = 41337;
const SECRET = 'ohja4Eif9eidah3aj5ta2haa4iemooth';
@@ -46,13 +46,14 @@ export default function createApi(log: Log, pg: Pg, discogs, events) {
const bundles = await pg.bundleCandidates();
return bundles.map((b: ListingData[]) => {
const total = b.reduce((acc, l) => acc + l.data.original_price.value, 0);
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,
price: l.data.original_price.value,
currency: l.data.original_price.curr_abbr,
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,
@@ -65,7 +66,7 @@ export default function createApi(log: Log, pg: Pg, discogs, events) {
seller: b[0]?.data.seller.id,
shipsFrom: b[0]?.data.ships_from,
total,
currency: b[0]?.data.original_price.curr_abbr,
currency: b[0]?.data.price.currency,
shipping: '...',
items,
mentionsAustralia: shipping.toLowerCase().includes('australia'),
@@ -108,17 +109,16 @@ export default function createApi(log: Log, pg: Pg, discogs, events) {
ctx.response.body = bundles;
})
router.post('/offers', async (ctx, next) => {
const { seller, shipping, baseShipping } = ctx.request.body;
ctx.response.body = await offers.generate(seller, shipping, baseShipping);
})
router.get('/user/offers', async function userOffersGet(ctx, next) {
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
const user = ctx.state.user;
const bundles = (await getBundles())
.map(b => {
b.items = b.items.filter(i => i.user.id === user.id);
return b;
})
.filter(b => b.items.length);
ctx.response.body = bundles;
const offers = await pg.userOffers(user);
ctx.response.body = offers;
});
router.post('/want', async function wantPost(ctx, next) {
+1 -1
View File
@@ -14,7 +14,7 @@ import { Pg } from './pg';
// vanderbabe
// jmcdermid1
export default function discogs(log: Log, pg: Pg) {
export default function createDiscogs(log: Log, pg: Pg) {
const TOKEN = 'Discogs token=wRTXuYZGOPsIQgKRGKCAkUkidGtvpSfQgPWkYIpD';
const apiBase = 'https://api.discogs.com'
const discogsBase = 'https://www.discogs.com'
+1 -1
View File
@@ -11,7 +11,7 @@ export enum Events {
StartupSync = 'StartupSync',
}
export function eventService(log: Log, pg: Pg, discogs) {
export default function createEvents(log: Log, pg: Pg, discogs) {
const events = new EventEmitter({ captureRejections: true });
events.on('error', function eventError(e) {
+52
View File
@@ -0,0 +1,52 @@
import type Log from 'bunyan';
import type { PrismaClient, Want, User, Listing, Offer, } from '@prisma/client';
import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';
import { logAxiosError } from './utils';
import { Pg } from './pg';
export enum Status {
Pending = 'pending',
Accepted = 'accepted',
Ordered = 'ordered',
Delivered = 'delivered',
}
export default function createOffers(log: Log, pg: Pg) {
async function generate(seller: number, bulkShipping: number, baseShipping: number): Offer[] {
log.info('generateOffers', { seller, bulkShipping, baseShipping });
const listings = await pg.listingFindSeller(seller);
log.info('creating offers for listings', { listings })
const shipping = bulkShipping / listings.length;
const offers = listings.map(l => ({
userId: l.want.user.id,
listingId: l.id,
wantId: l.want.id,
status: Status.Pending,
item: l.data.price.value,
shipping,
total: l.data.price.value + shipping,
// without mcrates...
comparison: l.data.price.value + baseShipping,
}))
// we must sort the offers by price,
// sometimes a seller has multiples of a want
// to deal with this we just ignore duplicates on insert
// and ensure the cheapest one goes in first
const sorted = sortBy(offers, ['item']);
const inserts = await pg.offersCreate(sorted);
console.log(inserts);
return inserts;
}
return {
generate,
};
}
+49 -4
View File
@@ -1,6 +1,6 @@
import bcrypt from 'bcrypt';
import type Log from 'bunyan';
import type { PrismaClient, Want, User, Listing, Prisma } from '@prisma/client';
import type { PrismaClient, Want, User, Listing, Offer, Prisma } from '@prisma/client';
import * as Bluebird from 'bluebird';
export interface UserCreateParams {
@@ -13,15 +13,20 @@ export interface Pg {
userFindEmail: (email: Email) => Promise<User>,
userCreate: (params: UserCreateParams) => Promise<User>,
userWants: (user: User) => Promise<Want[]>,
userOffers: (user: User) => Promise<Offer[]>,
listingUpsert: (listing: Listing, want: Want) => Promise<Listing>,
listingFind: (id: DiscogsListing) => Promise<Listing>,
listingFindSeller: (id: DiscogsSeller) => Promise<Listing[]>,
listingRmUnlisted: (want: Want, ids: DiscogsListing[]) => Promise<number>,
wantCreate: (discogsWant: any, user: User) => Promise<Want>,
wantList: (user: User) => Promise<Want[]>,
wantsToSync: () => Promise<Want[]>,
listingUpsert: (listing: Listing, want: Want) => Promise<Listing>,
listingFind: (id: DiscogsListing) => Promise<Listing>,
listingRmUnlisted: (want: Want, ids: DiscogsListing[]) => Promise<number>,
bundleCandidates: () => Promise<Listing[][]>,
offersCreate: (offers: Offer[]) => Promise<Offer[]>,
}
export default function createPg(log: Log, prisma: PrismaClient): Pg {
@@ -37,6 +42,15 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
return prisma.want.findMany({ where: { userId: user.id }});
}
async function userOffers(user: User) {
return prisma.offer.findMany({
where: { userId: user.id },
include: {
want: true,
}
});
}
async function userCreate(params: UserCreateParams) {
const hashRounds = 10;
const password = await bcrypt.hash(params.password, hashRounds);
@@ -131,6 +145,19 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
});
}
async function listingFindSeller(seller: DiscogsSeller): Promise<Listing[]> {
return prisma.listing.findMany({
where: {
seller,
},
include: {
want: {
include: { user: true },
}
}
});
}
async function bundleCandidates(): Promise<Listing[][]> {
const candidates = await prisma.$queryRaw`
select
@@ -157,11 +184,27 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
})));
}
async function offersCreate(offers: Offer[]) {
return await prisma.$transaction(
offers.map(offer => prisma.offer.upsert({
where: {
userId_wantId: {
userId: offer.userId,
wantId: offer.wantId,
},
},
create: offer,
update: offer
}))
);
}
return {
userFindId,
userFindEmail,
userCreate,
userWants,
userOffers,
wantCreate,
wantList,
@@ -169,7 +212,9 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
listingUpsert,
listingFind,
listingRmUnlisted,
listingFindSeller,
bundleCandidates,
offersCreate,
};
}
+10 -8
View File
@@ -1,17 +1,19 @@
import { PrismaClient } from '@prisma/client'
import discogsService from './discogs';
import pgService from './pg';
import createDiscogs from './discogs';
import createOffers from './offers';
import createLog from './log';
import createApi from './api';
import { eventService } from './events';
import createEvents from './events';
import createPg from './pg';
export default function createServices(log) {
export default function creates(log) {
const prisma = new PrismaClient();
const pg = pgService(log, prisma);
const discogs = discogsService(log, pg);
const events = eventService(log, pg, discogs);
const api = createApi(log, pg, discogs, events);
const pg = createPg(log, prisma);
const discogs = createDiscogs(log, pg);
const events = createEvents(log, pg, discogs);
const offers = createOffers(log, pg);
const api = createApi(log, pg, discogs, offers, events);
return {
api,