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
| List | Role | Key relationships |
|---|---|---|
CollectibleSet | A 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), tcgplayerGroupId | franchise, category, cards |
CatalogItem | One card printing (number, rarity, supertype, energy types, HP, artist, image URLs, tcgplayerProductId) or one sealed product (kind: sealed, sealedFormat) | collectibleSet, products, marketPrices, specimens |
Product / ProductVariant | Sellable 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, preorderLimitPerCustomer | catalogItem, prices (MoneyAmount) |
MarketPrice | Append-only reading per card and finish: low, mid, high, market (minor units), source, observedAt | catalogItem |
PricingRule | Per 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
Specimenis the physical object. ItslifecycleStatus(intake, review, available, reserved, sold, returned, withdrawn) is write-locked and moves only through system commands.InventoryUnitholds one specimen at oneInventoryLocation, with an ownership basis and a version fence.InventoryMovementis its ledger andReservationholds it for a cart, offer, auction, operator, or return. All three refuse generic GraphQL create and update.Listingpublishes one unit for sale. It is written only bymanageCollectibleListingandreceiveCollectibleItem.- Evidence lists are append-only:
ConditionObservation,GradeObservation(withGradingCompany),ProvenanceEvent,CustodyEvent, andValuationObservation. Each has apubliclyVisibleflag that controls what the storefront item record shows.
Buying, consignment, and collectors
Buylistholds the terms (cash and credit percentages, per-condition shares, minimum offer, public notes).BuylistEntrytargets a card printing with a minimum condition, hotlist flag, stock cap, quantity bought, and NM cash and credit offers.TradeInSubmissionandTradeInItemrecord 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), andConsignmentSettlementwithSettlementLinerecord consigned stock. Consignors areSellerProfilerecords.BinderandBinderCardrecord cards a collector owns (finish, condition, language, grade, quantity, cost basis).WishlistItemis the want list (card, finish, worst acceptable condition, maximum price).SavedSearchandCollectorProfilecomplete the customer side.FulfillmentRequirementandShipmentEvidencecover high-value shipping;DisputeCaseandEvidenceAttachmentcover disputes.
Operator workflows
Import the catalog and refresh prices
importCardCatalogimports one bundled checklist per call (features/keystone/lib/pokemon/checklists), creating theCollectibleSet, itsCatalogItemcards, and initial readings. It can also create an upcoming set and its sealed products.- On Market & Repricing, staff run
refreshMarketPrices. For every set with atcgplayerGroupIdit fetches the day's TCGplayer prices from TCGCSV, appendsMarketPricerows, and updates buylist entries whosepricingModeismarket. Per-set failures are reported, not thrown. - Staff preview
repriceInventory(apply: false), then apply it. Only variants withpricingMode: marketon products linked to a catalog card are considered. Each variant gets a rule by stock kind and condition; results arepriced,review(the move exceeds the rule's maximum change and is not applied), orskipped. Applying updates the base price, scales other currencies proportionally, and stampslastRepricedAt.
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→acceptedordeclined→returned. - Item:
pending→accepted→listed→sold; any active item can go toreturn_requested→returned. - Agreement:
draft→sent→accepted,expired, orterminated.
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 recordShipmentEvidence. - Disputes:
open→evidence_requested→review→resolved→closed, with assignee, resolution, and evidence attachments. - Sellers:
applicant→active(approve) orclosed(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:
placeCollectibleBidandsubmitCollectibleOfferthrow whileAUCTION_LIFECYCLE_RELEASEDandOFFER_LIFECYCLE_RELEASEDarefalse. - Shared commerce: cart and checkout,
transitionOrderStatus,confirmManualPayment,createOrderFulfillment,cancelOrderFulfillment, provider shipping labels and rates,processReturnRefund, invoices,adjustInventory,importInventory,registerWebhookEndpoint, andretryWebhookDeliveries.
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.