|

Collectibles architecture and workflows

Data model, card-shop operator workflows, state transitions, custom GraphQL commands, and access control in the current source.

Data model

Models live in features/keystone/models/**. Shared commerce lists come from Openfront E-commerce; collectibles lists are in the Collectible*.ts files. docs/schema-map.md in the source repository lists every list with its fields and states.

Cards, sets, and stock

ListRoleKey relationships
CollectibleSetA Pokémon set: name, code, series, printed and total card counts, release date, releaseStatus (a set can be a pre-order set before its card list exists), tcgplayerGroupIdfranchise, category, cards
CatalogItemOne card printing (number, rarity, supertype, energy types, HP, artist, image URLs, tcgplayerProductId) or one sealed product (kind: sealed, sealedFormat)collectibleSet, products, marketPrices, specimens
Product / ProductVariantSellable stock. A product links to its catalog card; each variant carries stockKind (raw, graded, sealed), cardCondition (NM, LP, MP, HP, DMG), cardFinish, cardLanguage, pricingMode, lastRepricedAt, and preorder, preorderReleaseDate, preorderLimitPerCustomercatalogItem, prices (MoneyAmount)
MarketPriceAppend-only reading per card and finish: low, mid, high, market (minor units), source, observedAtcatalogItem
PricingRulePer stock kind and condition: basis, percentage, minimum, rounding, maximum change, priority—

Raw singles are a quantity on a variant, so checkout decrements them like any product. A graded slab is a product with one graded variant tied to a serialized InventoryUnit, which points to a Specimen with a GradeObservation (company, grade, label, certificate, verification state) and a certificate SpecimenIdentifier.

Serialized items and evidence

  • Specimen is the physical object. Its lifecycleStatus (intake, review, available, reserved, sold, returned, withdrawn) is write-locked and moves only through system commands.
  • InventoryUnit holds one specimen at one InventoryLocation, with an ownership basis and a version fence. InventoryMovement is its ledger and Reservation holds it for a cart, offer, auction, operator, or return. All three refuse generic GraphQL create and update.
  • Listing publishes one unit for sale. It is written only by manageCollectibleListing and receiveCollectibleItem.
  • Evidence lists are append-only: ConditionObservation, GradeObservation (with GradingCompany), ProvenanceEvent, CustodyEvent, and ValuationObservation. Each has a publiclyVisible flag that controls what the storefront item record shows.

Buying, consignment, and collectors

  • Buylist holds the terms (cash and credit percentages, per-condition shares, minimum offer, public notes). BuylistEntry targets a card printing with a minimum condition, hotlist flag, stock cap, quantity bought, and NM cash and credit offers.
  • TradeInSubmission and TradeInItem record what a customer offers to sell, the inspected condition, quoted amounts, and accepted amounts.
  • ConsignmentIntake, ConsignmentItem, ConsignmentAgreement (commission in basis points, fixed fee, pricing authority, terms snapshot), and ConsignmentSettlement with SettlementLine record consigned stock. Consignors are SellerProfile records.
  • Binder and BinderCard record cards a collector owns (finish, condition, language, grade, quantity, cost basis). WishlistItem is the want list (card, finish, worst acceptable condition, maximum price). SavedSearch and CollectorProfile complete the customer side.
  • FulfillmentRequirement and ShipmentEvidence cover high-value shipping; DisputeCase and EvidenceAttachment cover disputes.

Operator workflows

Import the catalog and refresh prices

  1. importCardCatalog imports one bundled checklist per call (features/keystone/lib/pokemon/checklists), creating the CollectibleSet, its CatalogItem cards, and initial readings. It can also create an upcoming set and its sealed products.
  2. On Market & Repricing, staff run refreshMarketPrices. For every set with a tcgplayerGroupId it fetches the day's TCGplayer prices from TCGCSV, appends MarketPrice rows, and updates buylist entries whose pricingMode is market. Per-set failures are reported, not thrown.
  3. Staff preview repriceInventory (apply: false), then apply it. Only variants with pricingMode: market on products linked to a catalog card are considered. Each variant gets a rule by stock kind and condition; results are priced, review (the move exceeds the rule's maximum change and is not applied), or skipped. Applying updates the base price, scales other currencies proportionally, and stamps lastRepricedAt.

Receive stock

Receive Items (/dashboard/platform/items/receive) calls receiveCollectibleItem against an existing product variant. It creates the catalog entry, specimen, condition observation, inventory unit, and receipt movement, and can publish a listing in the same call. For slabs it records the grade and certificate and rejects PSA 9.5. It can take an item in on consignment with a consignor name and commission.

Sell

Singles and sealed product go through the standard cart (createActiveCart, updateActiveCart, updateActiveCartLineItem, addActiveCartShippingMethod, initiatePaymentSession, completeActiveCart). Pre-order variants are capped at the cart: the per-customer limit counts this cart plus the customer's earlier uncancelled orders; guests are limited per cart. Serialized items sell through their Listing, whose lifecycle is draft, published, withdrawn (with scheduled, paused, sold, and ended states). Orders paid by the bank-transfer provider are created with a pending payment and confirmed with confirmManualPayment.

Trade-ins

A signed-in customer submits a sell list with submitTradeIn. The dashboard trade-in desk moves submissions through:

draft → submitted → received → inspecting → quoted → accepted → paid, with submitted or quoted → declined → returned.

Quotes and inspections can change only while received or inspecting; quotes prefill from the buylist. Mark paid records the status only; it does not move money or issue store credit.

Consignment

  • Intake: submitted → received → inspecting → offered → accepted or declined → returned.
  • Item: pending → accepted → listed → sold; any active item can go to return_requested → returned.
  • Agreement: draft → sent → accepted, expired, or terminated.

Settlements are read-only on the consignment and seller pages; no code generates them.

High-value fulfillment, disputes, and sellers

  • High-value fulfillment: a requirements queue per paid order; staff set or replace a FulfillmentRequirement (declared value, signature, insurance, packaging) and record ShipmentEvidence.
  • Disputes: open → evidence_requested → review → resolved → closed, with assignee, resolution, and evidence attachments.
  • Sellers: applicant → active (approve) or closed (decline); active ⇄ paused; suspended → active (reinstate); any → closed. Identity evidence state is set separately.

Trade-in, consignment, dispute, and seller transitions are checked in the dashboard server actions (features/platform/*/lib) and then written with the list's update mutation. Role permissions protect the list, but a direct GraphQL client holding the same permission is not held to the transition map.

Custom GraphQL operations

Registered in features/keystone/mutations/index.ts:

  • Public queries: publicCardSets, publicSetChecklist, publicCardMarket, publicBuylist, publicCollectiblesDiscovery, marketplaceCollectiblesCapabilities, getProductsSortedByPrice.
  • Customer (owner-scoped): activeBinder, addToBinder, removeFromBinder, activeWantList, toggleWantList, activeWishlist, toggleWishlistListing, activeTradeIns, submitTradeIn, plus account, address, order, invoice, and privacy operations.
  • Staff collectibles commands: importCardCatalog (canManageCatalog), refreshMarketPrices (canManageCatalog), repriceInventory (canManageProducts), receiveCollectibleItem, manageCollectibleListing.
  • Disabled: placeCollectibleBid and submitCollectibleOffer throw while AUCTION_LIFECYCLE_RELEASED and OFFER_LIFECYCLE_RELEASED are false.
  • Shared commerce: cart and checkout, transitionOrderStatus, confirmManualPayment, createOrderFulfillment, cancelOrderFulfillment, provider shipping labels and rates, processReturnRefund, invoices, adjustInventory, importInventory, registerWebhookEndpoint, and retryWebhookDeliveries.

Access control

Access combines a session (cookie or of_ API key bearer token) with role permissions from features/keystone/models/fields.ts. Collectibles-specific permissions include canReadCollectibles, canManageCatalog, canManageListings, canManageAuctions, canManageConsignments, canManageEvidence, canManageCollectibleCustomers, canManageRisk, and canManageIntegrations, alongside the shared product, inventory, order, payment, fulfillment, return, discount, gift card, user, role, and webhook permissions. API keys are limited to their scopes and to the owner role's permissions.

StoreMembership stores staff roles in the store, but current access checks use role permissions, not memberships. Customer-owned lists are filtered to the signed-in human user.

On this page