Files
crates/api/src/offers.ts
T
2024-10-07 00:12:58 +11:00

154 lines
4.8 KiB
TypeScript

import type Log from 'bunyan';
import type { PrismaClient, Want, User, Listing, Offer, } from '@prisma/client';
import assert from 'assert/strict';
import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';
import flatMap from 'lodash/flatMap';
import { Context } from 'koa';
import { logAxiosError, wantDetails } from './utils';
import { Pg } from './pg';
export enum Status {
Complete = 'Complete',
Confirmed = 'Confirmed',
Accepted = 'Accepted',
Pending = 'Pending',
Declined = 'Declined',
}
function sortOffers(o) {
return Object.values(Status).indexOf(o.status);
}
export default function createOffers(log: Log, pg: Pg) {
async function generate() {
const listings = flatMap(await pg.interestingListings());
log.debug('generateOffers', { listings: listings.length });
const shipping = 7.5; // yolt
const offers = listings.map(l => ({
userId: l.want.user.id,
listingId: l.id,
wantId: l.want.id,
status: Status.Pending,
condition: l.data.condition,
item: l.data.price.value,
shipping,
total: l.data.price.value + shipping,
}))
// 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']);
// TODO probably need to take one of each condition of record from a store
// eg. they have a VG+ and a M we should make offers for each
log.trace('generated offers', sorted);
const inserts = await pg.offersUpdate(sorted);
return inserts;
}
async function accept(ctx: Context) {
const { id } = ctx.request.body;
ctx.assert(id, 401, 'no id supplied');
// check the user actually has this offer
const offer = await pg.userOfferFind(ctx.state.user, id);
return pg.offerStatusSet([offer.id], Status.Accepted);
}
async function statusSet(user: User, ids: UUID[], status: Status) {
log.info({ status, ids }, 'setting offer status');
// const offer = await pg.offerFindId(ids);
const update = await pg.offerStatusSet(ids, status);
// forget about this for now
// if (status === Status.Complete) {
// log.info({ id, status }, 'completed offer, removing want...');
// await pg.wantRm(offer.wantId);
// }
return update;
}
// converts offers into groups based on seller
// includes some bits of info about shipping etc
async function acpOffers() {
const offers = await pg.offersAdmin();
const grouped = groupBy(offers, o => o.listing.data.seller.id);
const sellerOffers = sortBy(Object.values(grouped).map(offers => {
const shipping = offers[0]?.listing.data.seller.shipping;
const bundle = {
username: offers[0]?.listing.data.seller.username,
seller: offers[0]?.listing.data.seller.id,
shipsFrom: offers[0]?.listing.data.ships_from,
shipping,
offers: sortBy(offers, sortOffers),
mentionsAustralia: shipping.toLowerCase().includes('australia'),
mentionsWorldwide: shipping.toLowerCase().includes('world'),
}
return bundle;
}), bundle => -bundle.offers.length);
return sellerOffers;
}
// also groups offers by seller for users
// but mostly just munges the structure so the details
// are easily available
async function userOffers(user: User) {
const offers = await pg.userOffers(user);
const groups = Object.values(groupBy(offers, (o) => o.wantId))
.map(g => {
// the offer status is just the most advanced status
// based on sort
const offers = sortBy(g, sortOffers);
const status = offers[0].status;
// now actually sort them by price
offers.sort((a, b) => a.total - b.total);
const details = offers[0].want.data;
return {
details,
offers,
status,
};
});
return groups;
}
async function samples() {
const user = await pg.userFindEmail('ntr@mnml.gg');
const offerGroups = await userOffers(user);
const i = Math.floor(Math.random() * offerGroups.length / 2);
return offerGroups[i];
}
return {
generate,
accept,
statusSet,
acpOffers,
userOffers,
samples,
};
}
export type Offers = ReturnType<typeof createOffers>;