Directory Theme class reference
Marketplace Theme — Class reference
The 79 classes that make _mv behave differently from the shared Directory core. Generated from the theme source.
← Back to Marketplace Theme features
AdminCommission AdminCommission.php
PremiumPress ▸ Commission (F12): the default rule, category rules and plan rules. Seller overrides live on PremiumPress ▸ Sellers (listed here read-only). Saving replaces the whole table (Commission::saveConfig). Rules are snapshotted onto each sale line, so an edit here never changes a past sale.
AdminMessages AdminMessages.php
PremiumPress > Messages: a READ-ONLY view of every buyer–seller conversation, so the owner can settle a dispute from what was actually said (HANDOFF-01 F19). Reads the shared Account\Messages table directly rather than through Messages::thread(), which marks the messages read for the user opening them — an owner looking at a thread must not change what either member sees as unread. Nothing here sends, edits or deletes. List: newest activity first, 50 a page, searchable by member name, email or shop name. Thread: every message in order, with who sent it, when, and any attachment.
AdminModeration AdminModeration.php
Marketplace moderation on Products > All Products (owner, 2026-09-28 — replaces the Moderation tab of the retired PremiumPress > Sellers screen): - the shared "Reported" tab (Admin\Listings::reportedIds()) also lists products this theme has HIDDEN (reports over the auto-hide threshold, or the owner), except the ones hidden only because their shop is paused — those come back with the shop; - the row menu gains "Hide listing", "Dismiss reports" (clears the reports and un-hides) and a link to the seller's member page (Users > edit), where Pause shop / Remove seller live. Actions go through Moderation::hide() / dismiss(), exactly as the old screen did.
AdminMoney AdminMoney.php
The Marketplace money summary on Checkout ▸ Payouts (owner, 2026-09-28): what buyers paid, what the site kept, and where every seller's share stands. sales item sales after discounts, in the chosen period (postage separate) commission what the site kept from those sales postage shipping buyers paid, passed straight to the sellers waiting sellers' shares on orders not shipped / delivered yet (Hold open) held sellers' shares shipped and inside the hold period (Hold held) disputed the part of held that a buyer's refund request is keeping there ready released to sellers' balances, not paid out yet (ledger pending, MV- refs) paid paid out to sellers (ledger paid, MV- refs) Refunded and cancelled sub-orders are left out, the same rule the seller's own dashboard uses (Hold::summary). The four balances are "now" figures; only sales, commission and postage follow the period. Static helper, not a Service.
AdminRefunds AdminRefunds.php
PremiumPress ▸ Refunds (F23): refund all or part of one shop's part of an order. The admin picks how the money goes back (RefundPayout, 2026-09-30): to the card through the gateway (Stripe / PayPal), as the buyer's account credit, or "already refunded elsewhere". Refunds::record() then keeps the seller's balance and the commission right.
AdminSellerCard AdminSellerCard.php
The Marketplace seller, inside the theme's own user system (owner, 2026-09-28): - PremiumPress > Users list: the account-type badge carries the seller's standing, "Seller · Pending" / "Seller · Open" / "Seller · Paused" … (filter ppt_users_account_type_label, Admin\Users::accountTypeLine()). - The member's edit page: a "Seller" card — status and dates, the shop, the application answers, the decision reason, the commission override, and the same Approve / Reject / Pause / Resume / Remove decisions as PremiumPress > Sellers. Every decision goes through AdminSellers::handle() (one code path, same emails); the card only adds return_member so the owner lands back on this page. The card sits inside the member page's save form, so its buttons submit standalone forms printed after that form (ppt_member_edit_after_form) through the form="" attribute.
AdminSellers AdminSellers.php
PremiumPress ▸ Sellers (F2, F22): Applications, Sellers and Moderation tabs. One admin-post action does every decision (ppt_mv_seller_do, with do = approve | reject | pause | resume | remove | commission | hide | dismiss), so each row's buttons are tiny forms and the result comes back as a toast on the same tab.
AdminSettings AdminSettings.php
PremiumPress ▸ Marketplace: the marketplace's own settings (_mv\Settings, option ppt_mv_settings) - who can sell, how long sellers' money is held, what can be sold and shown, reports, featured spots and reviews. Before this screen the settings could only be changed in code (owner, 2026-09-28). Every field is always posted (checkboxes carry a hidden 0), because Settings::save() keeps any key it is not sent - an unticked box would otherwise never turn off. stale_days is not shown: nothing reads it yet.
BuyerHub BuyerHub.php
The buyer's side of the Marketplace hub: a "Purchases" group with Downloads (F6) and To review (F18). Shown once the member has bought something from a shop - a digital file they own, or a sub-order that is theirs - so a member who only browses (or only sells) never meets two empty tabs. Wired into the shared hub views next to _mv\SellerHub: hub-nav-data.php asks nav() for the group, hub-panels.php prints panels().
Cart Cart.php
Shop cart storage — add/update/remove/list, backed by a dedicated DB table rather than a PHP session (session-based carts don't survive a session expiry or play well with page caching). Table pattern mirrors Ppt\_mv\ListingViews' versioned dbDelta-on-init install. A cart belongs to a cart_key: 'u' . user_id when signed in, otherwise a random id in a long-lived httponly cookie (merged into the user's cart on login so items survive signing in). Rows only store the qty/variant — everything else (title, image, live price, live stock) is read fresh from the listing on every request via items(), the same "meta is the source of truth" approach the old DT10 cart used, without reviving its $_SESSION storage.
Checkout Checkout.php
PPT Shop — checkout for a multi-item cart. A sibling to Ppt\Payments\CheckoutFlow (which only ever checks out ONE line — a listing plan or an ad zone), not a modification of it — CheckoutFlow.php is not touched by this class. Reuses the same shared machinery CheckoutFlow does: the ppt_payment_gateways registry and Stripe/PayPal plugins (Ppt\Admin\Payments), the exact ppt_gateway_start / ppt_gateway_verify filter contracts, the same ppt_orders CPT (Ppt\Admin\Orders), the same /checkout/ + /callback/ virtual pages (Ppt\Frontend\Pages), and the same receipt email templates (Ppt\Content\EmailTemplates). All price math (discount/shipping/tax) comes from Cart::totals(), which itself reuses CheckoutFlow::totals() for the tax portion — so the admin's Design ▸ Checkout settings (Coupons/Tax/Shipping/ Tracking) apply here exactly as they do to a listing-plan purchase, without duplicating that math. Order refs are prefixed MV- (vs CheckoutFlow's PP-) so Pages::content() can route an incoming /callback/ to whichever class actually owns that order, with no shared state between the two. Gateway webhooks: the Stripe and PayPal webhook listeners call \Ppt\Payments\CheckoutFlow::markPaid(), which hands an MV- order to self::markPaid() (CheckoutFlow::orderOwner(), by REF_PREFIX). So an order completes whether the buyer's tab returns to /callback/ or only the webhook arrives, and completeOrder() claims the order atomically so the two never both run it.
Comments Comments.php
Product Q&A on the Marketplace (owner, 2026-09-28) - buyers ask the seller a question on a product page; the seller (or anyone signed in) answers in the thread. It runs BESIDE star reviews (_mv\Reviews), never instead of them. The thread itself is the shared Directory\Qanda (threaded replies, up/down votes, the "Owner" badge, instant approval for the seller's own answers, the Q&A emails); this class only plugs it into the Marketplace, as _ct\Comments does for Classifieds: - ppt_qa comment type, stamped from the form's hidden ppt_qa marker; - a question is accepted on any product that has not switched Q&A off (ppt_comments_off), even when that product's REVIEWS are off - _mv\Reviews' comments_open rule speaks for reviews only; - the ask box, worded for a shop ("Ask the seller"). The product page template aliases this class as PptGlobalSingle\Comments.
Commission Commission.php
Marketplace commission rules (F12) — what the owner keeps from each sale line. A rule is {percent, fixed}: both may apply together (8% + $0.30). Resolution per line, first match wins (owner decision 2026-09-26, HANDOFF-01 s15.3): 0. product post meta ppt_mv_commission (JSON {percent, fixed}) on the product, set by the owner only (Commission screen > By product; owner decision 2026-09-28: the product rule wins over every other rule) 1. seller user meta ppt_mv_commission (JSON {percent, fixed}) on the seller 2. category the line's category in option ppt_mv_commission.categories, then that category's PARENT 3. plan the seller's effective membership plan in .plans (a plan's rate replaces only the default) 4. default .default (10% out of the box) commission = round(line value after discount x percent / 100 + fixed, 2), the fixed part charged once per line, and never more than the line value. Shipping is never commissioned — it is the seller's cost passed straight through. Rules are resolved and snapshotted onto each sub-order line at sale time (SplitOrders); a later edit here never changes a past sale. Static helper, not a Service (the Commission admin screen is Ppt_mv\AdminCommission).
Condition Condition.php
Item condition (spec field ppt_mv_condition, brought forward from [Later] for the Secondhand & Collectibles niche, owner decision 2026-09-27). One optional select on the product editor - New, Mint, Excellent, Good, Fair - stored as a key in product meta ppt_mv_condition. The spec listed New / Excellent / Good / Fair; Mint was added because the approved Secondhand designs grade collectibles on Mint / Excellent / Good / Fair, and New keeps it right for new goods. Empty = not stated, and nothing is shown. Where it shows: the product page, under the price ("Condition: Excellent" with its one-line meaning); every live card whose block has an item{N}_condition field (via LiveShops::liveRow() and the condition key in Blocks\Support\LiveItems). A static helper, not a Service: it needs no hooks of its own.
DemoAccounts DemoAccounts.php
Marketplace — what the one-click DEMO members walk into (spec s11). The home-demo page offers a Buyer login (test_client) and a Seller login (test_solo) through Account\DemoLogin; the plain test member is furnished as a buyer too. Both build on the demo MARKET (_mv\DemoMarket): the three shops and their products. A demo site needs no listings (owner's ruling, 2026-10-02), so when the site has no market the first Buyer / Seller login BUILDS one sized to the story (buildMarket): only the products the buyer's orders name, four for the shop the seller borrows and one per shop - about ten rows, demo-marked, torn down with the accounts - never the 20-row catalogue. A site whose Sample data already made a market simply uses it. SELLER. The demo seller BORROWS Hollow Clay Studio: the shop's owner meta and its products pass to them (products through DemoLogin's own lend list, so its purge hands them back), with the shop's shipping profile. Then real marketplace orders are placed and split through SplitOrders, exactly as a paid basket would be: - seven to ship (four "ordered", three "packed"), - one shipped five days ago, so its money is HELD, - six older ones delivered and RELEASED to the Ledger, which is the balance the Earnings panel offers to withdraw (its exact figure follows the site's commission settings - the spec's "$612" is the default-rate figure, not a stored number). BUYER. Three parent orders, split across the three shops: delivered two weeks ago (the mug pair from Hollow Clay + Paper Moth's digital pattern, which the split GRANTS - it is in Downloads), shipped last week (Fern & Thread's napkins) and packed today (Hollow Clay's stacking rings). The buyer follows two shops and has a thread with Fern & Thread. DEMO ONLY. A demo member's seller application and withdrawal request are answered with "demo only" instead of being filed. Every fixture is removed on unfurnish (DemoLogin's purge), which also hands the borrowed shop back. Order e-mails are not sent while the fixtures are built.
DemoActivity DemoActivity.php
Sample shop activity for the Marketplace demo (owner, 2026-09-28): a year of product views and a year of orders for every demo shop, so the Seller Hub dashboard (views, visits, orders, sales, the chart, top tasks, "sales" in the header) shows real numbers the moment the sample data is installed. views 12 months per shop, growing from ~6 to ~22 a day across the year, busier at weekends, spread over the shop's live products (some more popular than others); about three views in four come from a new visitor orders about 1.2% of visits, placed through the demo order builder the demo login already uses (DemoAccounts::placeOrder), so they split into sub-orders, appear in Orders and pay out like real ones: 10+ days old delivered, money released (withdrawable) 5-9 days shipped, money held 3-4 days packed (overdue against the 3-day dispatch) 0-2 days ordered, waiting to be sent Deterministic (seeded by the shop), so a reinstall shows the same shape. Everything is tagged: view rows carry user_name VIEW_TAG and orders ORDER_META, and remove() clears both. Runs at the end of DemoMarket::seed() and at the start of DemoMarket::remove().
DemoMarket DemoMarket.php
The Marketplace demo MARKET (spec s11) — what "Sample data" builds on a marketplace site beyond the 19 generic sample rows: the three shops that sell them. seed() (called by Tools\SampleData::seed() after it writes the rows, and idempotent): - three demo sellers — Hollow Clay Studio (Portland), Fern & Thread (Asheville), Paper Moth (Providence) — approved, each owning a shop term with a banner (the studio photo from the Kiln design, sideloaded as a demo attachment of the shop's first product, so deleting the demo rows takes it too), About copy, policies, a location and a shipping profile: Domestic $4.50 +$1, Europe $12 +$3, Rest of world $19 +$4, local pickup — except Fern & Thread, whose domestic rate is $3.00 (owner decision, the s9 fixture); - the mvhandmade rows handed out round-robin in row order (row N → shop (N-1) % 3, the order rows.md and the designs use), stock tracked, the one-of-a-kind walnut bowl at 1, every "Digital Products" row turned into a real digital download with a small demo PDF in the private store; - seller stars on six of each shop's sample reviews, then the shop averages; - the moderation queue's two demo items: Northbank Leather's pending application and one open buyer report on a product (below the auto-hide threshold). The demo sellers carry user meta _ppt_mv_demo_seller (their key) and the applicant _ppt_mv_demo_applicant; shop terms are Demo::markTerm()ed. remove() takes all of it away again (Tools\SampleData::removeDemo()). They are not login accounts: the one-click Seller demo BORROWS Hollow Clay Studio (_mv\DemoAccounts).
DemoOptions DemoOptions.php
Real, editable OPTIONS on the demo products that would really have them (owner, 2026-09-28) — replacing the invented "Color: Black/Navy/Sand/Olive + Size: S–XL" the Shop fork put on every demo product in preview. One catalogue, keyed by the demo product's title (Content\SampleListings rows), used two ways: - Sample data: when a product is flagged as demo (the demo meta is written after the product exists — the same hook _cb\Cashback uses), its options and per-combination prices are SAVED to the product, so the seller editor shows them and they can be changed like any real product's. - Design preview: the showroom's in-memory product (Frontend\DemoListing::SYNTH_ID) reads the same options, so the preview and the installed demo agree. Each choice carries an extra charge (0 = the product's own price). Combinations are stored the way the editor stores them: a full price per combination, written only where it differs from the base price. Secondhand items are one of a kind and get none.
Download Download.php
Digital download products (F6). Adapted from _so\Download, with the files kept in the SHARED Media\PrivateStore (_so's own store lives in the _so fork, which a Marketplace ZIP does not ship). THE PRODUCT. ppt_mv_type = digital (the editor's "What are you selling?" switch, which also turns shipping and stock off). Its files are PrivateStore names, not media-library attachments — outside the library, unguessably named in a denied folder, streamed only through code — kept as a JSON list in ppt_mv_download_files ({stored, name, size, mime}), at most MAX_FILES of up to 200 MB each (or the host's upload limit, when lower). The seller manages them in the product editor through two AJAX calls, so the shared editor form needs no multipart encoding. TWO STEPS, so a working file URL can never be shared (as _so): 1. ACQUIRE (admin-post ppt_mv_get, the buyer's Downloads button): signed-in owner only; answers with a redirect to a freshly SIGNED delivery URL. 2. DELIVER (?ppt_mv_dl=… on template_redirect): HMAC-signed, bound to the buyer and the file, valid TTL (300 s). Re-checked before streaming: signature, expiry, the signed-in user IS the buyer, and they still own it (Purchases — a refund revokes). A signed-out visitor holding a copied link is sent to sign in; an expired one is refused. The seller and the site owner can fetch their own files too. allow_digital (_mv Settings, default 1) switches the whole feature off: the editor offers no digital type and nothing new can be sold as one. Existing owners keep access.
Emails Emails.php
The Marketplace's email templates (HANDOFF-01 section 7), added to the shared catalog through ppt_email_catalog — Content\EmailTemplates::send() refuses unknown keys, and this puts them on Settings ▸ Emails under their own "Marketplace emails" group where the owner can edit or switch each one off. Everything sends through self::send(), which fills the tokens every template shares.
Expiry Expiry.php
PPT — listing expiry / "Listing lifetime". The Settings ▸ Listings "Listing lifetime" (days) governs how long a listing stays live. 0 = never expires. A value > 0 stamps an expiry timestamp on each listing when it's published; a daily cron then runs the configured "On expiry" action (listings_expiry_action: nothing / draft / pending / trash) once the time is up. The expiry timestamp meta is SHARED with the pricing-plan expiry (PricingPlans::LISTING_EXPIRES_META). A listing that carries a pricing plan is governed by that plan's own duration (set in the editor / at checkout), so the global lifetime only applies to listings WITHOUT a plan. Either way, the cron here enforces whatever expiry timestamp a listing ends up with, and both editors show the time remaining.
FeaturedSpots FeaturedSpots.php
Featured spots (F21) — a seller pays to put their SHOP in the homepage "Featured makers" row for 1-4 weeks. Bought as a single line through Payments\CheckoutFlow's fork-item path: the hub's Featured panel sends the seller to /checkout/?item=mvfeatured:featured_price per week, _mv Settings) on ppt_checkout_item — again on the pay POST, so a form can never set the amount — and fulfils it on ppt_checkout_item_paid. The money is the site owner's, like any CheckoutFlow order: it never touches the seller Ledger. Fulfilment stamps the shop's term: ppt_mv_featured_until (unix ts; a paid extension runs on from the current expiry, never overlaps it) and ppt_mv_featured_at (the purchase time — the row shows the newest purchase first). Expiry needs no cron: every reader compares the timestamp with now, so a shop simply drops out of the row the moment its week ends. featured_slots (default 8) caps how many shops may be featured at once. A full row sells nothing new — the panel says when the next slot frees up — but a shop already featured may always extend, since it holds its slot.
Gallery Gallery.php
Single-listing gallery styles. The admin picks one of four layouts on the Design ▸ Listings tab (stored in the ppt_design option via Branding), and the single-listing template renders the chosen layout for the listing's images. standard — large hero photo + thumbnail strip (click to swap) [default] grid — all photos in a tiled grid (first one featured) carousel — a swipeable/scroll-snap slider with prev/next tall — full-width photos stacked vertically (good for portraits)
Geocoder Geocoder.php
Bulk geocoder — fills in lat/lng for existing listings that have an address but no coordinates (e.g. listings created before the editor's map picker existed), so they get a precise map marker and the "Distance from me" feature. Uses the site's Maps provider (Settings ▸ API keys): Google / Mapbox geocoding APIs (their key), or OpenStreetMap Nominatim (keyless, rate-limited to ~1 req/sec with an identifying UA). A small box on the PPT Listings screen runs it in batches over AJAX. Listings that can't be geocoded are flagged (_ppt_geo_failed) so they aren't retried forever.
Hold Hold.php
Seller earnings and the holding period (F13) — the money state of each sub-order. Copied in shape from Ppt_ct\Escrow (states, snapshot on the order, hourly cron sweep), but per SUB-ORDER and purely time-based — there is no buyer "received" button: open paid, not shipped yet. Stays here until the seller marks it shipped; a sub-order still open after stale_days is flagged stale for the owner. held shipped (or digital-only, which is held at payment). hold_until = held time + hold_days (default 14; owner decision 2026-09-26: the clock starts at SHIPPED, not delivered). released the sweep credited the seller's NET to the shared Payouts\Ledger, under the sub-order's own ref (MV-<parent>-<n>), at a 100% share: the net is already after commission, and accrue()'s rate is the payee's SHARE. refunded / cancelled closed without paying the seller (F23 records these). Every transition is guarded on the state it comes from, and Ledger::accrue() is idempotent on the ref, so a sweep that runs twice pays once.
IdCheck IdCheck.php
Seller ID checks (owner, 2026-09-28): a seller sends a photo of an ID document, the marketplace looks at it, and the shop shows "ID verified". PRIVACY (owner decision: delete after the decision) - the document goes straight into Media\PrivateStore (a non-public folder, random file name) - never the media library, never a public URL; - only an admin can open it (manage_options + a nonce), as a download; - it is deleted the moment the admin approves or declines; only the outcome, the document type and the dates remain. A declined seller can send a new one; - deleting the member deletes any document still waiting. FLOW Seller Hub > Shop settings "Verify your identity" card -> PremiumPress > Sellers > ID checks (view, approve, decline with a reason) -> the seller is emailed and belled; an approved shop shows the badge on its storefront. Marketplace setting id_check: optional (default) | off. Being verified never gates selling here - switching a gate on would stop every existing shop at once. User meta ppt_mv_idv (JSON): {status: pending|approved|rejected, type, files:[{name, mime}], submitted, decided, reason}.
ListingActions ListingActions.php
Per-listing visitor actions shown on the single-listing section nav: - Add to favorites — toggles the listing in the member's saved list (user meta ppt_favorites); the account page shows them. Logged-in only; guests are routed to sign-in. - Report — flags the listing to the site admin. The reason is stored as a comment on the listing (type ppt_report, a custom approval status so it stays hidden from the front-end and the normal comment-moderation tabs) and also emailed to the admin. Reports are reviewed in the admin Comments screen's dedicated "Reports" tab. Open to guests and members.
ListingCard ListingCard.php
The canonical listing card — the single, theme-wide way a business/listing is shown as a card. Every directory surface (search results, single-page "related", and the live listing-grid blocks) renders through here, so a listing looks the same everywhere and the demo-vs-live data split lives in ONE place. Data-source agnostic: fromPost() maps a real listing_type record and fromSample() maps a curated demo row into the same normalized shape, which render() draws. The markup is built on the theme design tokens, so the one card automatically adopts each design's colours/fonts. Its CSS is enqueued site-wide (assets/css/listing-card.css) — the card only emits markup. Normalized shape: name, link, img, cat, rating, desc, desc_long, price, city, dist, featured (bool), highlighted (bool), badge (string), id, buyable (bool), sold_out (bool) badge is an optional ribbon label (e.g. "New") for callers that need one; when empty it falls back to "Featured" if featured is set, or "Sold out" when sold_out is set (which also overrides "Featured"). Every card on this fork carries the same footer: the price on the LEFT and an "Add to cart" pill on the RIGHT. Which of three shapes that pill takes is decided by buyable / sold_out: - buyable (a product price, no variant group, in stock) → a real <button> that adds to the cart in place (assets/js/shop-cart.js). - sold_out → the same pill, disabled, reading "Sold out". - anything else with a price (variants to choose, or a demo/preview card with no post behind it) → the pill as a <span>, so the click falls through to the card's own link and lands on the product page where options are picked. It can't be an <a>: the whole card already is one, and anchors can't nest. Only a listing with NO price at all falls back to the "View details" text CTA — a card offering to add a priceless thing to a cart would be a lie.
ListingEditor ListingEditor.php
Listing editor — the shared brain behind the two listing-edit screens: - Admin : PPT-styled screen at ?page=ppt_listings&edit=<id> (Chrome shell), rendered by Admin\Listings when the edit param is present. - Member : front-end screen at /account/listing/<id>/ inside the Member Hub, rendered by Account\MembersPage for a listing the member owns. Both POST to admin-post.php (action ppt_listing_save) and run through the one save() below, so the field set, sanitising and persistence live in a single place. The mode (admin|member) decides which extras are honoured: admins get status, author, slug, featured/verified and the "edit only" custom fields; members get a friendly subset scoped to their own listing. Fields = core (title, description, category, tags, featured image, gallery, FAQ) + every field defined in the Custom Fields admin (Admin\Fields / option ppt_fields), stored as post meta keyed by the field key — which is what the single-listing template already reads.
ListingFaq ListingFaq.php
Single-listing FAQ. Reads per-listing FAQ items from the faq post meta (an array of ['q' => …, 'a' => …]). A live listing shows only its own saved FAQ — when it has none the section is omitted rather than filled with fabricated entries. Demo/preview mode supplies sample FAQ via DemoContent so a fresh design isn't empty. Populate a listing's own FAQ by saving faq meta, inject a site-wide default set via the ppt_listing_faq_default filter, or adjust the final list with the ppt_listing_faq filter.
ListingHours ListingHours.php
Business hours display for the single-listing sidebar. Reads the business_hours meta written by the listing editor (ListingEditor::HOURS_META) and renders a Google-Business-style day list with an "Open now / Closed now" badge computed in the site's timezone. Times are formatted with the site's time format.
ListingLocation ListingLocation.php
Single-listing "Location" section — the richer location block: the full formatted address, an interactive map (ListingMap, precise when the listing has coordinates), a "Get directions" button, and a "Distance from you" control (browser geolocation → straight-line distance to the listing). Self-contained: it prints its own scoped CSS + JS once, so single.php just calls render().
ListingMap ListingMap.php
The single-listing "Location" map. Uses the site's Maps provider setting (Settings ▸ API keys — the same ppt_maps_provider filter the search map reads). When the listing has stored coordinates (lat/lng, set by the editor's map picker) the map centres on them precisely and drops a named marker; otherwise it falls back to geocoding the address string: google / mapbox(fallback) → a keyless Google Maps embed osm → a Leaflet map (marker from coords, else Nominatim) Map libraries load from their CDNs — the owner-approved front-end map exception.
ListingSections ListingSections.php
PPT — configurable single-listing sections. The admin (Design ▸ Listings ▸ "Listing page sections") gets a sortable list of on/off toggles for the optional feature blocks a listing page can carry — Booking, Business hours, Maps & Location, FAQ, Reviews. Turning one OFF removes it from BOTH the single listing page and the listing submission/edit form; the order of the list controls the order the (main-column) sections appear in. Config lives in the shared design option (Branding::OPTION = ppt_design) under the listing_sections key: { order:[keys…], enabled:{key:0|1} }. Anything not saved yet defaults to ON, in the catalog order — so existing sites are unchanged until the admin edits the list.
ListingSpecs ListingSpecs.php
Single-listing Specifications — the long-form spec copy a product page carries under its description (materials, sizing, care, what's in the box…), stored as sanitised HTML in the specifications post meta. Deliberately NOT the same thing as the custom-field "Details" list on the same page: that one is a structured label→value table defined site-wide in the Custom Fields admin, whereas this is free-form copy written per product, with its own sub-headings and bullet lists. They render as two separate accordion sections. Demo/preview mode falls back to DemoContent::specifications() so a fresh design isn't empty, exactly as ListingFaq does — nothing fabricated is ever written to the database, and a live product with no specs simply omits the section.
ListingViews ListingViews.php
PPT listing analytics — records a "hit" every time a single listing page is viewed, and serves that data back to the listing owner as a graph in the member area (/account ▸ My listings ▸ the analytics icon). Each view stores the listing id, the timestamp (UTC), and — when the visitor is logged in — their user id + display name, so an owner can see who has been looking. Bots/crawlers are skipped so counts reflect real visitors. The owner (and admins) can read the analytics for a listing over the AJAX endpoint, which returns a daily series (for the chart), rolling-window totals (7/30/60/90 days) and a list of recent named viewers. Storage: a custom table {prefix}ppt_listing_views (dbDelta on init).
LiveShops LiveShops.php
Live shops for the Marketplace homepage designs (Kiln / Loom / Market Hall). The product grids are ordinary listings blocks, so LiveItems fills them with real products. This class makes those cards speak marketplace: through ppt_live_items_row the "who posted it" slot becomes the SHOP (name, monogram, logo), a product with no city of its own takes the shop's, and a digital product carries the "Instant download" tag the designs print on the photo. The maker rows and spotlights are text blocks, which LiveItems never touches. They call fill() / spotlight() here instead, with the same contract as every other overlay: the showroom (PreviewMode::showSamples()) keeps the curated makers; a live site with at least one open shop shows its real shops, featured first then the busiest, and blanks the curated slots it could not fill, so an invented studio never sits beside a real one; a live site with no open shop yet keeps the curated row whole.
MarketCategories MarketCategories.php
The Marketplace theme's shipped CATEGORIES — ten main categories, NO sub-categories (owner decision 2026-09-27; replaced the earlier 8-parent / 63-child tree): Fashion & Accessories · Electronics & Gadgets · Home & Furniture · Handmade & Crafts · Digital Products · Services & Freelancers · Beauty & Personal Care · Food & Local Produce · Rentals & Bookings · Secondhand & Collectibles Kept in the Ppt_so\SoftwareCategories shape (slug => [label, parent slug]) so a site owner can still add sub-categories of their own; every shipped row is top level. Categories are the shared listing_category (Ppt\PostTypes\Listings::TAX_CATEGORY); only the seed set is marketplace-specific, so it lives in the fork. The seed is one-shot AND empty-only: a site that already has categories keeps them. Demo seeding calls ensureShipped() directly (Tools\SampleData::ensureProfileVocabulary). Every shipped term is tagged ppt_category_profile = marketplace, and the product editor's picker is filtered to this profile's terms, as _rt\PropertyCategories does — so a Shop's "Baby clothing" left behind never appears among the marketplace's own.
Media Media.php
Listing images — the single, WordPress-native resolver for the PPT image system. A listing's photos are its WP featured image (primary) plus any image attachments parented to it (the gallery). No legacy ?imgid refs, no image-URL meta strings — everything flows through the media library.
MemberShop MemberShop.php
One shop per seller — the storefront an approved seller owns (F3/F4). Cloned in shape from Ppt_cp\MemberStore (2026-09-26). A seller OWNS exactly one mv_shop term, and every product they list is filed under it automatically, so a product can never be unfiled or filed under someone else's shop. OWNERSHIP IS RECORDED TWICE, deliberately: term meta _ppt_shop_owner => user id the authority user meta ppt_mv_shop => term id the index shopFor() verifies the index against the authority on every read and repairs it when they disagree. Differences from the coupon original: a shop is created by APPROVING a seller (Ppt_mv\Sellers calls ensureFor()), never by a member simply saving a form; a shop name is unique and a clash is an error, not a silent "(2)" suffix (F4); and there is no admin-curated shop — every mv_shop term has an owner. The Shop settings hub panel (renderPanel) arrives with F4.
Moderation Moderation.php
Moderation and buyer reports (F22). Buyer reports are the existing ppt_report comments (ListingActions::ajaxReport, which fires ppt_listing_reported). When a product reaches report_autohide open reports (default 3; 0 = off) it is hidden pending review: product meta ppt_mv_hidden = reports, and the owner gets mv_report_received. ppt_mv_hidden is the one "keep it out of the marketplace" switch — set by reports, by the owner ("Hide listing"), or by a shop pause (Sellers). A hidden product leaves search and facet counts (searchMetaClause, merged into _mv\SearchPage's query), and its page answers 404 to everyone but its seller and the owner. The owner works the queue at PremiumPress ▸ Sellers ▸ Moderation (AdminSellers): pending applications, hidden products and reported products, with Approve / Hide / Pause shop / Remove seller / Dismiss.
Notify Notify.php
Marketplace events on the notification bell (owner, 2026-09-28). The emails stay as they are; this adds the on-site alert beside them, each linking to the right screen: seller a new order ("New order MV-123-2: 2 items") -> the order a refund request / an escalated request's decision -> the order a buyer asked for changes to delivered work -> the order earnings released from the hold -> Earnings shop approved -> the Seller Hub buyer their order shipped / delivered, or a service delivered -> tracking / Services the seller cancelled their order -> Order help a refund request answered or decided, a refund recorded -> Order help Questions on a product already reach the seller's bell (Directory\Qanda). All pushes go through Notifications\Center::push(), which also feeds the mobile app's push.
Personalise Personalise.php
Personalisation (Etsy comparison, owner 2026-09-28): a seller can let buyers type the text a product should carry — a name, initials, a date — the way Etsy's "Add personalization" works. The seller sets, per product (post meta META): on whether the product takes personalisation at all text instructions shown to the buyer ("Enter the name for the label, 15 letters max") limit most characters the buyer may type, 1-1024 (default 256) optional whether the buyer may leave it blank The buyer's text rides the basket row (Cart's personal column), so two of the same product with different names stay two lines, and is snapshotted onto the order lines (personal beside variant) so the seller sees it on the order. No extra charge: a seller who charges for it builds that into the price, as on Etsy.
Product Product.php
Shop product data for a listing — price, stock, and a Shopify-style multi-group variant matrix (e.g. Size × Color). Replaces Ppt\Directory\ListingPricing's service-tier concept (Basic/Standard/Premium plans) with a real product for sale, entered on the submission form and stored as post meta. Field choices are checked against Shopify's product/variant object (the de facto standard shape for this kind of data): price + compare_at_price (the "was" price, not "sale_price", so price always reads as what the buyer pays now), sku, barcode, inventory tracking on/off + quantity + oversell policy, requires_shipping (so a digital/service item can skip shipping), and weight (kg) — consumed by the shipping-rate resolver (Ppt\_mv\Shipping) and any live carrier-API provider plugin (UPS, Royal Mail, …), which rate-quote by shipment weight. Variants: an admin defines one or more independent GROUPS (e.g. "Size" with values Small/Medium/Large, "Color" with values Red/Blue) — mirrors Shopify's option1/option2/option3. On save, ListingEditor::saveProduct() expands the cartesian product of every group's values into COMBOS (one row per real-world choice, e.g. "Small / Red"), each with its own optional price/sku/stock override. A combo is identified everywhere downstream (cart rows, order lines, the buy-box's hidden variant field) by a single string key — the group values joined with " / ", in group order — so the rest of the system (Cart, Checkout, the variant post field) never needs to know it's multi-dimensional; it just carries an opaque key that happens to be human-readable. On save, the base price also mirrors into the same searchable price meta (SearchPage::PRICE_META) that ListingPricing used to mirror the cheapest plan into — so the price sort/filter/range and the card price field keep working with zero changes.
ProductCsv ProductCsv.php
Product CSV import / export for sellers (owner, 2026-09-28). Seller Hub > Shop settings has the card; the Products list links to it. EXPORT every product the seller has (any status), one row each: id, sku, title, description, price, compare_at_price, stock (blank = not tracked), shipping (yes/no), weight, categories and tags (" | " between), status, has_options, url. UTF-8 with a BOM so spreadsheet apps read accents; a cell that starts with = + - @ gets a leading apostrophe so it can't run as a formula. IMPORT the same columns (any order, only title needed for a new product): - a row with the id of one of the seller's products updates it; else a sku that matches one of their products; else it makes a NEW product - always as a DRAFT, so the photo, category, terms and payment rules still apply when they submit it; - an update changes the columns present and leaves the rest; a live product goes back to review when the site moderates edits (as the editor does); - a product with options (variants) keeps its option prices and stock - the price and stock columns are skipped for it; - categories must already exist (name or slug); unknown ones are reported; tags are capped at the editor's 13; - status is never taken from the file; images are added in the editor; - at most MAX_ROWS rows and MAX_BYTES; bad rows are skipped and reported by line.
ProductList ProductList.php
The seller's Products list tools (Etsy comparison, owner 2026-09-28), Marketplace only: search title, tag or SKU, filtering the rows as you type filters All · Live · Draft · In review · Expired · Sold out, each with a count bulk tick rows, then Renew (expired), Hide from shop (live → draft) or Delete The shared hub listings panel (Account/views/parts/hub-panels.php) renders the table; on the Marketplace it asks this class for the toolbar, each row's data attributes and the tick box in the first column. The rows are all on the page - on the Marketplace the hub loads up to LIMIT products (ppt_hub_listings_limit, 50 elsewhere) and shows PAGE at a time with "Show more" - so search and filters run in the browser across every loaded product; the bulk actions post to BULK_ACTION, which checks every id belongs to the seller.
ProductSeller ProductSeller.php
The seller on a Marketplace product page (HANDOFF-01 F8), drawn under the buy box by templates/single-shop.php: card() the shop card — logo, name, verified mark, rating, followers, Follow, Message shop (Account\Messenger, the product pre-filled), Visit shop — then the delivery lines: "Shipping from $4.50" (or Free shipping / "Does not ship to …") from the seller's OWN shipping profile, Ships from <city>, the dispatch time, and Local pickup when offered. moreFromShop() "More from <shop>": up to four of the seller's other live products. Built into the buy-box block rather than as a board card of its own, the way the Auction theme's "Bid box & seller" is, so it follows the buy box wherever the owner places it (Directory\SectionPlacement) and needs no new board entry. The destination for the estimate is the visitor's country when the request carries one (a CDN country header), else the seller's home country — the estimate a no-address cart shows too (ShippingProfiles::zoneFor('') = domestic).
ProductSeo ProductSeo.php
Product SEO on the Marketplace (owner, 2026-09-28): what a product looks like in Google results and when shared. - Per product, an optional search title (up to 70 characters) and description (up to 200), set in the seller's product form. Blank = the product title and the start of its description. - With no SEO plugin, the theme prints them: the <title>, a meta description, and Open Graph tags (title, description, image, price) for link previews. - With Yoast or Rank Math active, the plugin prints its own tags; a product's typed title and description are handed to it through the plugin's filters. - Always: schema.org Product JSON-LD - name, description, images, SKU, the offer (price, or a low-high range across options; currency; stock; condition; the shop as seller) and the star rating when the product has reviews - so Google can show price, stock and stars in results.
Purchases Purchases.php
Digital purchases (F6) — which downloads a buyer owns. Adapted from _so\Purchases: a per-user ledger in user meta, one row per (sub-order, product), granted when the order is paid (the sub-orders exist: ppt_mv_suborders_created) and removed when that sub-order is cancelled or fully refunded. Re-downloads are unlimited while a row stands. Rows: {sub, listing, price, currency, date}. sub is the sub-order id, which is what a cancel/refund names, so revoking takes exactly the rows that order granted and never a second copy the buyer paid for separately.
RefundPayout RefundPayout.php
Paying a marketplace refund (owner, 2026-09-30) - the money half; Refunds::record() is the accounting half (seller balance, commission) and runs after the money has moved. The admin picks, per refund, in PremiumPress > Refunds (with a confirm step): card back to the buyer's card through the gateway, via the refund contract: apply_filters('ppt_gateway_can_refund', false, $gatewayId, $ctx): bool apply_filters('ppt_gateway_refund', null, $gatewayId, $ctx) -> array{ok:bool, id:string, message:string} | null (not supported) $ctx: order_id (the buyer's whole order), ref, amount, currency (the order's recorded currency), reason, idempotency_key. Stripe and PayPal implement it (plugins ppt-stripe / ppt-paypal), and the Test gateway for local testing. Capped at what the CARD paid: the order total less any account credit, less earlier card refunds on the same order (parent meta ppt_mv_card_refunded). credit into the buyer's account credit (StoreCredit) - any gateway, any order. manual refunded outside the site; recorded with its reference (the old flow). Nothing is recorded unless the money moved. A per-sub-order lock stops a double submit sending two refunds; the gateway also gets an idempotency key.
RefundRequests RefundRequests.php
Buyer refund requests and disputes (owner, 2026-09-28), per sub-order - one shop's part of an order. Refunds.php keeps doing the money; this is the conversation that decides whether, and how much, to refund. open the buyer asks: a reason (not received, not as described, damaged, cancel before it ships, other), an amount up to what is refundable and a note. The note is also posted to the buyer-seller message thread, which is where both sides add photos and detail. The seller has 3 days. approved the seller accepts (full or part), or the marketplace approves. A full refund on an order that has not shipped (or a service not yet delivered) also cancels it and restocks (Refunds::sellerCancel). The owner then sends the money in Stripe / PayPal and records it on PremiumPress > Refunds, as before; once recorded the request reads "refunded". declined the seller says no, with a reason. The buyer can ask the marketplace to step in for 3 days; after that the request closes. escalated the buyer asked the marketplace, or the seller did not answer in 3 days. The owner approves an amount or closes it. closed withdrawn by the buyer, closed by the marketplace, or a decline that was not escalated. A buyer can ask while the seller's money is still waiting or held (Hold open / held), i.e. until earnings release; one request per sub-order. While a request is open, declined, escalated or approved, Hold::release() does not auto-release the sub-order, so the seller is never paid out in the middle of a dispute. There is no return (send-it-back) step. Meta on the sub-order: ppt_mv_request (JSON), ppt_mv_request_status (for queries).
Refunds Refunds.php
Refunds and cancellations (F23) — per sub-order; the other shops in the order are never touched. Money moves in Stripe/PayPal by hand (the theme has no gateway refund API); this RECORDS it and puts the seller's balance right: refund R on a sub-order: commission back = commission x min(R, items value) / items value (pro rata) seller share = R - commission back before release (open / held): net -= seller share; a full refund closes it (state refunded) and nothing is ever accrued after release, entry pending and not locked by a withdrawal: Ledger::void(the entry) and re-accrue the reduced net as <ref>-r<n> after release, entry paid or locked: flagged "Recover from seller" for the owner A seller may cancel their own sub-order while it is ordered / packed (restocks the items); the owner then records the refund against it. Meta on the sub-order: ppt_mv_refunds (JSON list), ppt_mv_recover (amount the owner must recover from the seller), ppt_mv_cancel_reason.
ReviewPhotos ReviewPhotos.php
Photos on product reviews (owner, 2026-09-28). A buyer may add up to 3 photos to their review; they show as thumbnails under it (each opens full size) and go with it when the review is deleted. Validation, not permissions, protects this (same approach as Account\Avatar::store()): only real uploads (is_uploaded_file), read to confirm they are images, JPEG/PNG/WebP only for the call, 5 MB each. A bad file is skipped - it never costs the buyer their review. Photos are only stored for a review that passed SellerReviews::guard(), i.e. a buyer whose order of the product has arrived. Storage: comment meta ppt_review_photos = [attachment ids]; each attachment carries _ppt_review_photo = the comment id and is NOT attached to the product (post_parent 0), so it never joins the product's own gallery.
Reviews Reviews.php
Listing reviews — built on native WordPress comments so they moderate and store like any comment, but stamped with the comment type "review" (the identifier that distinguishes them from ordinary blog comments in the admin) and a 1–5 star rating saved as comment meta. Reviews are always enabled on the listing CPT. Rendered in the single-listing "Reviews" (summary + breakdown + cards) and "Add Review" (star input + comment form) sections.
Roles Roles.php
Shopper or Seller: who this member is on the marketplace profile (F1). Sign-up asks "I want to shop" / "I want to sell" in its role step and stores the answer in ppt_signup_role; this bridges that answer onto the type Account\AccountType understands (buy → CLIENT, sell → SOLO) and supplies the words. Cloned from Ppt_ct\Roles (2026-09-26). A seller answer only says what the member WANTS. Whether they may list anything is the seller application's call (Ppt_mv\Sellers, ppt_mv_seller_status); a buyer can later click "Open a shop" in the hub, which starts that application on the same account. Gated to the marketplace profile by Theme::boot()'s fork-gating (this is Ppt_mv*).
SalesCount SalesCount.php
Units sold per product — what the "Best selling" sort orders by. Counted once per paid marketplace order, on the same ppt_mv_order_paid event that splits it (Checkout::PAID_HOOK fires after the order is atomically claimed). The order is stamped so a replayed hook never counts it twice. Refunds do not take units back: the figure is "how often this sells", not stock. Orders paid before this existed are counted by a one-shot backfill on the first admin request.
SearchFilters SearchFilters.php
The Marketplace search facets beyond Category / Price / Rating (HANDOFF-01 F7, plus the owner-approved extras of 2026-09-27): Shop tax_mv_shop the generic taxonomy facet over mv_shop; this class only trims its term list to open shops with products Item type mv_type ?mvtype=physical|digital (product meta ppt_mv_type) On sale sp_onsale ?onsale=1 (_mv\SearchPage, Product flag) Free shipping mv_freeship ?freeship=1 seller's shipping profile In stock mv_instock ?instock=1 Product stock meta Ships from mv_shipfrom ?shipfrom=GB seller's origin country Dispatch time mv_dispatch ?dispatch=1|3|7 product days, else shop default Seller mv_sellers ?sverified=1 / ?stoprated=1 verification + seller rating The keys are declared in Admin\Search::filterKeys() (so each has an admin toggle and a place in the drag order), drawn by Directory\Filters\Rail, and applied to the main query and the facet counts by _mv\SearchPage through metaQuery() / queryArgs() below. Seller-level facts (shipping profile, shop country, rating, verification) live on the SELLER, not the product, so those facets resolve to an author__in / post__in list here rather than a meta_query. Nothing is copied onto products, so nothing can drift when a seller edits their shipping or shop settings. Definitions (stated once, here): - Free shipping: a physical product whose seller's DOMESTIC zone is on with a first-item rate of 0, or whose own price reaches the seller's "free over" amount. Digital products are not listed (they have no shipping at all). - In stock: stock not tracked, stock above 0, backorders allowed, or digital. - Dispatch time: the product's own dispatch days, else the shop default, else 3. - Top-rated: seller average of TOPRATED_MIN or more over TOPRATED_COUNT+ reviews.
SearchPage SearchPage.php
Server-rendered search / archive for the listing post type — the clean PPT rebuild of DT10's PPT\Custom\Search\SearchPage. Runs the normal WordPress main query (WP_Query) via official hooks (pre_get_posts + template_include) so it is SEO-friendly and every core filter applies. Contexts it owns: - keyword search (/?s=…) - the listing post-type archive (/listing/) - listing category / tag archives Filters (GET, all optional, combinable): s keyword tax-listing_category category term id price1 / price2 min / max price (numeric, price meta) sort featured | newest | oldest | rating | price_low | price_high | title paged pagination
SellerCoupons SellerCoupons.php
Shop coupons (owner, 2026-09-28): a seller makes their own discount codes in the Seller Hub ("Coupons" tab) and hands them out however they like. Money: the SELLER pays for the discount. A shop code's discount is worked out on that shop's items only and allocated only to that shop's lines (SplitOrders::plan's $onlySeller), so it lands in the shop's sub-order M_DISCOUNT and commission is taken on the discounted price - the marketplace never funds it. Other shops in the same basket are untouched. net = subtotal - discount - commission + shipping still holds. One code per basket, shared with the marketplace's own codes (Checkout > Coupons): the cart's coupon box accepts either. A code is unique across every shop and the marketplace's list, so redemptions are counted by the same Payments\Coupon::uses() table (CouponRedemptions counts any completed order's code). Storage: user meta ppt_mv_coupons on the seller (a list of rows), plus the option ppt_mv_coupon_index [CODE => seller id] for the lookup at the till. Marketplace switch: Settings seller_coupons (PremiumPress > Marketplace).
SellerHub SellerHub.php
The seller's side of the member hub (F15), plus the Shop settings (F4) and Shipping (F11) panels, and the "Held" card on the shared Earnings tab (F13). Wired into the shared hub by three marked edits (the hub has no panel filter): hub-nav-data.php -> SellerHub::nav() rows + which panels exist hub-panels.php -> SellerHub::panels() the <section> per tab hub-scripts.php -> the deep-link allowlist (#sell, #mvoverview, #mvorders, …) A seller only ever sees THEIR sub-orders and the addresses on them (F15 rule).
SellerPlans SellerPlans.php
Seller plans (F20) — how many products a seller may have live, set by their membership plan, plus the lower commission a paid plan buys. THE ROW. mv_listings is a PERIOD_NONE quota on core's Account\Entitlements registry: a standing figure compared with a live count, never an allowance that is used up. It is edited in the ordinary PremiumPress ▸ Memberships grid and resolved through Account\Membership, so a lapsed paid plan falls back to the free plan (effectivePlan()) with no code here. The commission side needs nothing new either: _mv\Commission already resolves seller → category → PLAN → default, with plan rates on the Commission screen. WHAT COUNTS. Published products, as the spec says, for the publish gate; published AND pending for "may I start a new one?", so a seller at the limit cannot queue fifty submissions for an owner to approve in one batch (the lesson _cp\Entitlements records). THE GATES. 1. "Add product": at the limit no new draft is made (ListingEditor::handleNew calls refuseNew()), and the seller lands on their Plan panel, told why. 2. Publishing: a product moving to publish while its seller is already at the limit is put back to draft (transition_post_status) — the backstop for every other way in. The owner (manage_options) acting on a seller's product is an override and is let through. Products already live when a plan lapses STAY live (the spec): only the transition is gated. Both fail OPEN until some enabled plan actually sets a limit (gated()), so a site that has not configured memberships is never locked out. THE LADDER. ppt_memberships_sample_set hands "Generate 3 sample plans" the spec's Starter (free, 25 products), Maker ($12/mo, 100) and Studio ($29/mo, unlimited) - the same ladder the Agora pricing block shows (owner, 2026-09-28); when those plans are saved, their commission rates (12 / 9 / 7 %) are filled in on the Commission screen if the owner has not set any.
SellerReviews SellerReviews.php
Product and seller reviews (F18), on top of _mv\Reviews. WHO MAY REVIEW. Only a buyer with a paid sub-order that contains the product: a sub-order exists only once its parent order is paid (SplitOrders), and a cancelled or refunded one does not count. The item must also have reached the buyer — delivered, or 7 days since it shipped (the seller's hold starts at "shipped", so Hold's held-at stamp IS the ship time) — while a digital item can be reviewed as soon as it is paid for. One review per product per buyer. The guard runs on preprocess_comment, so a hand-made POST is refused exactly like the form. TWO RATINGS. The product's stars stay the rating comment meta _mv\Reviews already averages onto the listing; the SELLER's stars are mv_seller_rating, averaged over every approved review on the seller's products into user meta ppt_mv_rating_avg / _count (what the storefront header, Storefront::rating(), reads). Every change to a review — new, approved, unapproved, trashed, deleted, edited — recomputes it. SELLER REPLY. Once per review, in public: a comment of type review_reply whose parent is the review, written by the product's seller (admin-post ppt_mv_review_reply). It is never counted as a review, since every read of reviews asks for type review. The buyer's hub lists what they can review now (BuyerHub "To review").
Sellers Sellers.php
Sellers: the application, and the owner's approve / reject / pause / resume / remove decisions (F2). The one place that answers "may this member sell right now?". Seller user meta (HANDOFF-01 s4.2): ppt_mv_seller_status (pending | approved | rejected | paused | removed), ppt_mv_application (JSON answers), ppt_mv_applied_at, ppt_mv_decided_at, ppt_mv_decision_reason. - Only an APPROVED seller may publish products or take orders (admins always may). - Approval creates the mv_shop term (MemberShop::ensureFor) from the application. - Pause is reversible: the shop closes and its products leave search, tagged ppt_mv_hidden = shop_paused so resuming restores exactly those (a product the owner hid for another reason stays hidden). - Remove also drafts every product; a removed seller can re-apply only if the owner resets them. - seller_approval = auto approves on submit.
SellerStats SellerStats.php
The Seller Hub Overview's dashboard, from the Etsy comparison (owner, 2026-09-28): header shop logo, star rating and review count, items sold, shop link top tasks Orders (to ship, overdue) · Messages (unread) · Products (sold out, expired, awaiting review), each a link to where it is dealt with stats views, visits, orders and sales for 7 / 30 / 90 / 365 days or a custom from-to range (up to 2 years), each against the period of the same length before, with a daily (or monthly, past 90 days) views chart tips live products that would sell better with more photos, tags or a longer description ("Improvement suggestions" on Etsy's listing stats) Views and visits come from the listing-views table (_mv\ListingViews): a view is one page load, a visit is one visitor on one day. Orders and sales are the seller's own sub-orders, refunds and cancellations left out (Hold::summary()).
ServiceOrders ServiceOrders.php
Service orders: work delivered online, not posted (owner, 2026-09-28). A sub-order is a SERVICE when nothing in it ships (no line is physical) and at least one line is not a download - a logo package, website copy, a WordPress fix. Such a sub-order used to go straight into the hold at payment and read "Delivered online" before the freelancer had started. It now runs its own short flow: In progress - from payment. It stays OPEN (not held), so the seller can still cancel and refund it (Refunds::sellerCancel, the unshipped-goods rule). It is due on the order date plus the longest delivery time of its lines (ProductSeller::dispatchDays - the "7 days" on the card), and counts as overdue after that. Delivered - the seller clicks "Mark as delivered", with an optional short note (a link, "files sent to your email"). The hold starts THEN (Hold::hold), exactly as shipping starts it for goods, so earnings release a hold period after delivery; the buyer is emailed (mv_service_delivered) and may review straight away. Downloads, shipped goods and every Fulfilment stage are untouched: a service sub-order carries no address and no stage, so parcel tracking never sees it. A mixed sub-order (a download plus a service) follows this flow; its download stays instant.
ServiceRevisions ServiceRevisions.php
Revisions on service orders (owner, 2026-09-28). After a freelancer marks work delivered, the buyer may ask for changes - as many times as the product includes. Included each product sets "Revisions included" (0-5, default 1) in the editor. The sub-order snapshots the highest count among its lines when it is created, so a later edit to the product never changes a past sale. Asking the buyer (Purchases > Services) writes what to change. Allowed while the order is delivered and its money is HELD - never after the earnings are released, and not while a refund request is open. Effect owner decision: the payout clock restarts. The hold goes back to open (Hold::reopen), the order reads "In progress" again with a new due date (now + the delivery time), and the seller is emailed and belled. When the seller delivers again, the hold starts again from that moment. Sub-order meta: M_ALLOWED (int), M_USED (int), M_DUE (timestamp while a revision is open), M_LOG (JSON list of {type: delivered|revision, at, note}).
Settings Settings.php
The marketplace's own settings — option ppt_mv_settings (HANDOFF-01 section 8). Deliberately its own option, not a block inside the shared ppt_settings sanitiser: that sanitiser rebuilds the whole array from one POST, so a profile-gated block it never printed would be zeroed on save (the same reason _ct\Escrow keeps its own). Static reader, not a Service; PremiumPress > Marketplace (_mv\AdminSettings) writes it.
Shipping Shipping.php
PPT Shop — shipping for the cart. The rate-resolution logic now lives in the shared Ppt\Checkout\Shipping (so the Shop cart and the Auction lot payment price shipping identically); this class is the Shop's thin façade over it, keeping the existing _mv\Shipping::quote() call sites working and adding the one Shop-specific piece the shared engine can't own — summing a cart's line weights from each product (Product::weight()). See Ppt\Checkout\Shipping for the full 1-6 resolution order (disabled → restricted-country block → live carrier API → manual per-country rate → weight bands → flat rate, then the "free over" threshold).
ShippingProfiles ShippingProfiles.php
Per-seller shipping profiles (F11). Every shop ships its own parcels, so every shop charges its own postage. A profile is seller user meta ppt_mv_shipping (JSON): method items (default) | weight | price - how a zone's charge is worked out zones domestic | europe | world => {first, extra, days, on, bands} domestic = the shop's own country (ShopTax location), europe = the rest of Europe, world = everywhere else; bands = up to 6 {upto, rate} rows for the weight / price methods pickup {on, place, places} — collect in person, charged 0 when the buyer picks it; places = up to 5 named places (place = the first, kept for old code) free_over shop subtotal at or over which shipping is free (0 = off) A shop's shipping, over that shop's PHYSICAL lines only (digital lines never ship): items the zone's first-item rate + extra-item rate x (units - 1) weight the first band whose "up to" covers the parcel's weight (product weight x qty); heavier than every band = the last band's rate price the same over the shop's item subtotal A weight / price zone with no bands falls back to the items rates. A destination in a disabled zone blocks that shop: "Does not ship to <country>". A seller with no profile falls back to the site-wide Ppt\Checkout\Shipping::quote(). Static helper, not a Service (the hub "Shipping" panel that writes it is F11's UI).
ShopChecklist ShopChecklist.php
The shop setup checklist on the Seller Hub Overview (Etsy comparison, owner 2026-09-28): Etsy's "Stock your shop to start selling" banner and its "Customize your shop" list with a progress bar, which ticks each step off as the seller does it. Steps (each links to where it is done): shop name · logo · banner · your story (About) · seller photo (profile picture) · shipping rates · returns policy Etsy's list is the five branding steps; shipping rates and a returns policy are added because on this marketplace every shop charges its own postage and states its own returns, and a buyer sees both before paying. Once every step is done the list folds into a one-line "Your shop is set up" card the seller can close for good (user meta DISMISS_META). The "Stock your shop" banner shows while the shop has no live product, whatever the checklist says.
ShopTax ShopTax.php
Shops — the sellers of a marketplace, as a real taxonomy (F3). Every approved seller owns exactly one mv_shop term (Ppt_mv\MemberShop), and every product they list is filed under it. The term is where the shop's public identity lives: name, logo, banner, about, policies, location. It gives the marketplace two public surfaces: /store/<slug>/ one seller's storefront (the ordinary listing archive for the term) /shops/ the directory of every open shop (Frontend\Pages' shops slot) …and the "Shop" facet on search. Cloned in shape from Ppt_cp\StoreTax (2026-09-26), minus everything that exists for affiliate feeds (merchant-meta mirroring, network logos, back-fill): a marketplace shop is only ever created by approving a seller. Term meta (HANDOFF-01 s4.2): _ppt_shop_owner (user id, the authority — see MemberShop), _ppt_term_image (logo, the shared term-image key so live-category blocks show it for free), _ppt_shop_banner, _ppt_shop_about, _ppt_shop_policies (JSON), _ppt_shop_location (JSON {city, country}), ppt_mv_featured_until (F21). Gated to the marketplace profile by Theme::boot()'s fork-gating (this is Ppt_mv*).
SingleListing SingleListing.php
Single-listing page for the listing_type post type — the clean PPT rebuild of DT10's PPT\Custom\Single\SingleListing. Owns template_include for singular listings and renders its own token-styled template inside the normal main loop. No legacy $CORE / membership plumbing; standard WordPress access rules apply.
SplitOrders SplitOrders.php
One checkout, split orders (F10). The buyer pays ONCE for a basket spanning many shops — one parent ppt_orders post, ref MV-, made by Ppt\_mv\Checkout. When it is paid (`ppt_mv_order_paid`) this creates one SUB-ORDER per shop: a `ppt_orders` post with `post_parent` = the parent, ref `MV-`<parentId>`-`<n>, carrying that shop's lines, shipping, commission and net. The money, per HANDOFF-01 section 9: - a site-wide coupon's discount is allocated to lines pro rata by line value, the LAST line taking the rounding remainder, so the allocations always sum exactly; a SHOP's own code (SellerCoupons) is allocated the same way over that shop's lines only, so that seller alone pays for it; - commission is resolved per line (Commission::resolve, a product's own rate first) on the value AFTER its share of discount, and snapshotted on the line with the rule that produced it; - seller net = items after discount - commission + that shop's shipping; - tax and the parent total stay on the parent (the owner is merchant of record), so sum(net) + sum(commission) = parent total - tax. Idempotent: a replayed callback or a second firing of the hook never splits twice. Sub-orders are excluded from the owner's revenue figures (they have post_parent > 0; Admin\Orders and Admin\DashboardData count parents only).
StoreCredit StoreCredit.php
Account credit (store credit) for marketplace buyers (owner, 2026-09-30). Where it comes from a refund the admin chooses to pay "as account credit" (RefundPayout) - never bought, never withdrawn. What it buys anything at marketplace checkout: "Use my $X credit" pays part or all of an order. It is a PAYMENT, not a discount - the sellers' sub-orders, commission and net are exactly as for a card sale, and the owner funds it (the money never left them). Expiry never. A money balance in the store's base currency, not the whole-number Credits wallet the other themes sell. User meta: ppt_mv_credit the balance, "12.50" ppt_mv_credit_ledger JSON statement [{t:+/-, amount, balance, ref, note, at}], last 200 ppt_mv_credit_refs refs already applied - add()/take() are idempotent on a ref, so a double submit or a replayed callback never counts twice Every change runs under a per-buyer MySQL lock, so two checkouts at once cannot both spend the same credit.
Storefront Storefront.php
The seller's storefront and the shop directory (F3). /store/<slug>/ the ordinary listing archive for the shop's mv_shop term (the _mv SearchPage + shared search template draw the product grid, sort and paging); this class adds the shop header above the results — banner, logo, name, location, verified badge, seller rating, followers with a Follow button, "Message shop", and About / Policies tabs. A paused or removed shop shows a "closed" notice instead (its products are already out of search: Sellers stamps them hidden). /shops/ every open shop (page slot shops, added through ppt_page_slugs, rendered from Frontend\Pages::content()). Follow is Account\Follows (user-to-user): following a shop follows its owner.
SubOrderFulfilment SubOrderFulfilment.php
Sub-order fulfilment and tracking (F16). Each seller moves THEIR sub-order through ordered → packed → shipped → delivered with the shared Checkout\Fulfilment (stage, carrier, tracking number, dated history): - only the sub-order's own seller (or the owner) may move it, forward only; - "shipped" needs a carrier and a tracking number, unless the buyer collects; - "shipped" starts the seller's hold (Hold::hold, F13) and emails the buyer mv_suborder_shipped; - a refunded / cancelled sub-order cannot be moved. The buyer sees one tracker per shop: the parent order's parcels ARE its physical sub-orders (ppt_tracking_parcels), each labelled with its shop. The parent reads "Partly shipped" until every parcel has gone (parentStatus()). Also here: the seller's "new order" email on the split, and the printable packing slip (/account/?mv_slip=<subId>).
TermMeta TermMeta.php
Per-term IMAGE + ICON for the listing taxonomies. Adds two custom fields to the native WordPress term add/edit screens (Categories, Tags and any custom listing taxonomy): a media-library image picker and a swatch picker over the theme icon library. The values are stored as term meta and consumed by the live-category blocks (via Blocks\Support\Categories) so an admin can give each category a branded photo and icon instead of the auto-derived listing photo / fallback glyph. Admin-only UI; the read helpers (image()/imageId()/icon()) are safe to call anywhere. Taxonomies are resolved live from the listing post type, so new custom taxonomies get the fields automatically.
Uploads Uploads.php
Listing media uploads — the AJAX backend for the Uppy uploader on the listing editor (replacing the WordPress media frame). Uppy's XHRUpload posts one file per request to ppt_listing_upload; each becomes a normal WordPress attachment (so Media::gallery()/Media::videos() and the single-listing template keep working). Files are left unattached (post_parent = 0) until the listing form is saved, which parents the final set (ListingEditor::save()). A companion ppt_listing_media_remove deletes a freshly uploaded, not-yet- saved attachment when the user removes it in the editor, so abandoned uploads don't pile up in the media library.
Vacation Vacation.php
Vacation (away) mode for a shop (owner, 2026-09-28). A seller pauses orders while they're away, optionally until a date, with a short note for buyers. While away: - no new orders: ppt_mv_seller_can_sell answers false, so Add to cart is refused ("This shop is not taking orders right now") and a basket holding the shop's items can't check out - the same path a paused shop already takes; - products stay listed and their pages open, with an "away until…" notice above the buy box; the storefront shows the same notice and the seller's note; - orders already placed carry on as normal. Away ends by itself after the return date (no cron: every check compares the date). User meta on the seller: ppt_mv_away ('1'), ppt_mv_away_until (Y-m-d, '' = until turned off), ppt_mv_away_note.