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, id: UUID, status: Status) { log.info({ status, id }, 'setting offer status'); const offer = await pg.offerFindId(id); const update = await pg.offerStatusSet(offer.id, status); 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;