1
0
Fork 0
polaris-task-force/src/collections/market/MarketNegotiations.ts
Z8MB1E 89b8474e23 feat(market): add trading marketplace with listings, haggling, and NPC vendors
Tarkov-style flea market where users list locker items at a fixed price
and buyers pay through the banking system; items transfer directly
into the buyer's locker.

- market-listings + market-negotiations collections and the market page
  with search/rarity filters and a Countdown timer on each card
- service lib (buyListing with partial-quantity + negotiated-price
  support, clean-stock rule, creditLockerQuantity, auto pricing)
- negotiation flow: make/accept/reject/counter/withdraw, strictly-
  increasing buyer offers, NPC vendor auto-resolution
- NPC vendors: game-npcs attribution, patience meter, dialogue, chat
  bubbles, historical threads dialog
- 30d user / 7d auto listing durations, market-tick bin for expiry +
  vendor restock, SSE notify on tick
- market event types + game-npcs/market target collections in the
  event log; sidebar Market entry under Logistics
- partial-quantity purchases (buyListing/buyMarketListing/makeMarketOffer)
  with full int test coverage (25 tests)
2026-08-10 16:05:57 -04:00

155 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { CollectionConfig, Where } from "payload";
import type { User } from "@/payload-types";
import hasRoles from "@/utils/access-control/hasRoles";
import { isDeveloper } from "@/utils/access-control/isRole";
export const MarketNegotiations: CollectionConfig = {
slug: "market-negotiations",
admin: {
group: "Market",
useAsTitle: "id",
defaultColumns: ["id", "listing", "buyer", "amount", "patience", "proposedBy", "status", "updatedAt"],
pagination: { defaultLimit: 50 },
},
access: {
read: ({ req }) => {
const user = req.user as User | null;
if (!user) return false;
if (hasRoles(["admin", "developer"], user)) return true;
return {
or: [
{ buyer: { equals: user.id } },
{ "listing.seller": { equals: user.id } },
],
} as Where;
},
create: ({ req }) => !!req.user,
update: ({ req }) => {
const user = req.user as User | null;
if (!user) return false;
if (hasRoles(["admin", "developer"], user)) return true;
return {
or: [
{ buyer: { equals: user.id } },
{ "listing.seller": { equals: user.id } },
],
} as Where;
},
delete: isDeveloper,
},
fields: [
{
name: "listing",
type: "relationship",
relationTo: "market-listings",
required: true,
index: true,
admin: {
description: "The market listing this negotiation thread belongs to.",
},
},
{
name: "buyer",
type: "relationship",
relationTo: "users",
required: true,
index: true,
admin: {
description: "The player interested in buying the listing.",
},
},
{
name: "status",
type: "select",
required: true,
defaultValue: "open",
options: [
{ label: "Open", value: "open" },
{ label: "Accepted", value: "accepted" },
{ label: "Rejected", value: "rejected" },
{ label: "Closed (patience exhausted)", value: "closed" },
{ label: "Cancelled", value: "cancelled" },
{ label: "Expired", value: "expired" },
],
index: true,
},
{
name: "patience",
type: "number",
defaultValue: 0,
min: 0,
max: 100,
admin: {
description:
"NPC vendor patience (0–100). Rises each bargaining round and sharply for lowball offers; at 100 the negotiation closes and the buyer can only buy at the asking price.",
},
},
{
name: "quantity",
type: "number",
min: 1,
defaultValue: 1,
admin: {
description: "How many units the buyer wants from the listing.",
},
},
{
name: "amount",
type: "number",
required: true,
min: 0,
admin: {
description:
"The per-unit price currently on the table. Proposed by the buyer or the seller depending on the proposal.",
},
},
{
name: "proposedBy",
type: "select",
required: true,
defaultValue: "buyer",
options: [
{ label: "Buyer", value: "buyer" },
{ label: "Seller", value: "seller" },
],
},
{
name: "acceptedPrice",
type: "number",
admin: {
description: "The per-unit price agreed when the negotiation is accepted.",
},
},
{
name: "history",
type: "array",
fields: [
{
name: "amount",
type: "number",
required: true,
},
{
name: "from",
type: "select",
required: true,
options: [
{ label: "Buyer", value: "buyer" },
{ label: "Seller", value: "seller" },
],
},
{
name: "note",
type: "text",
},
{
name: "at",
type: "date",
required: true,
defaultValue: () => new Date().toISOString(),
},
],
},
],
timestamps: true,
};