| name | pallastrade-data-model |
| description | Use when the user is asking how PallasTrade's domain models relate — Orders, LineItems, Variants, Products, Stores, Channels, Markets, Payments, Shipments, Customers, Adjustments. Architecture and relationships only. Common phrasings include "how does X connect to Y", "what's the relationship between", "where does PallasTrade store X", "how do I query orders across stores", "how do channels work", "what's the difference between Cart and Order", "Store vs Channel vs Market". For adding new models / new API resources, use the `pallastrade-resource` skill. For field-level detail, see `docs/developer/core-concepts/` in the installed `@pallastrade/docs` package. |
PallasTrade Data Model
A relationship map for the most-asked-about PallasTrade models. Field-level documentation lives in the installed @pallastrade/docs package at node_modules/@pallastrade/docs/dist/developer/core-concepts/.
The catalog → cart pipeline
Product → Variant → LineItem → Order
- Product is the brand-level entity (name, slug, description, category).
- Variant is the sellable SKU. Every Product has at least one Variant. Variants carry SKU, prices, dimensions, and link to inventory.
- LineItem links a Variant to an Order with
quantity and price frozen at add-time.
- Order is the customer's transaction — the cart-in-progress and, after checkout, the completed transaction (same record, different
state).
Variants relate to stock via StockItem (one per Variant per StockLocation) and the StockMovement history.
Master vs default variant
A Product has a master variant (legacy concept, is_master: true) and a computed default_variant method: when PallasTrade::Config[:track_inventory_levels] is on, the first purchasable variant; otherwise the first non-master variant by position; master is only the fallback when the product has no other variants. product.default_variant_id just returns that computed variant's id. Neither is a database column in 5.5 — don't query or migrate against default_variant_id (a default_variant_id FK on pallastrade_products is planned for 6.0, implementation not started; see docs/plans/6.0-remove-master-variant.md). Use product.variants for the non-master sellable variants and product.variants_including_master only when you genuinely need the master row included.
Back-in-stock subscriptions
PallasTrade::BackInStockSubscription (table pallastrade_back_in_stock_subscriptions)
captures a guest email for one product: store_id, product_id, email, status
(active → notified). Unique per [product_id, email]; the Store API
POST /api/v3/store/products/:id/back_in_stock_subscriptions is idempotent and
re-activates a notified row. PallasTrade::BackInStockSubscriber emails active rows on
product.back_in_stock and marks them notified. A Store has_many back_in_stock_subscriptions.
Product reviews (P0-4)
PallasTrade::Review (table pallastrade_reviews) captures a customer review for one product:
store_id, product_id, user_id, rating (1–5), title, body, status
(pending → approved | rejected, default pending), verified_purchase (boolean, auto-set
from the customer's completed orders). Unique per [product_id, user_id] (a customer reviews a
product once). has_prefix_id :rev (URL-safe rev_… ids). SingleStoreResource (store-scoped).
A Store has_many :reviews; a Product has_many :reviews + has_many :approved_reviews.
Aggregation: Product#average_rating (average over approved, nil when none) and
Product#review_count (approved count) are exposed on ProductSerializer and feed the
storefront JSON-LD AggregateRating. Only approved reviews are public via the Store API
GET /api/v3/store/products/:id/reviews; moderation happens in the admin
PallasTrade::Admin::ReviewsController (approve / reject / delete).
Blog posts (CMS)
PallasTrade::Post (table pallastrade_posts) is the CMS blog article model.
A Store has_many :posts. Key fields: store_id, title, slug (FriendlyId,
unique per store), excerpt, author, published_at, seo_title, seo_description.
published_at nil = draft, a future value = scheduled, past/now = published
(post.published? / post.scheduled?).
It reuses the same infrastructure as PallasTrade::Policy:
PallasTrade::TranslatableResource — title/excerpt/seo_title/seo_description
are translatable (Mobility, pallastrade_post_translations table).
- ActionText rich body —
body is a per-locale rich text field (RICH_TEXT_TRANSLATABLE_FIELDS).
has_one_attached :cover_image (ActiveStorage).
Scopes: published / drafts / scheduled / newest_first
(published_at DESC NULLS LAST). The Store API only ever exposes published posts.
The multi-channel / multi-store axis
Store → Channel → ProductPublication → Product
Available since PallasTrade 5.5.
- Store is the top-level brand (one organization = one Store, typically).
- Channel is a selling surface within a Store: the online storefront, in-person POS, marketplace integrations (Amazon, eBay), B2B wholesale, mobile apps. Every Store has at least a default Channel named "Online Store".
- ProductPublication is the join: which Products are visible on which Channel, with optional
published_at / unpublished_at windows for scheduling.
- Order has
channel_id so revenue can be attributed per channel.
The Store API resolves a channel per request from the X-PallasTrade-Channel header (matched against channels.code or a ch_… prefixed ID); without it the store's default channel is used. The Admin API does not consume X-PallasTrade-Channel — admin queries return data across all channels for the current store.
Markets (regional config)
Market has_many :countries
Market columns: currency (string), default_locale (string)
Order belongs_to :market
A Market is a regional configuration: its set of countries, currency, and default locale. Stores typically get a default Market created automatically (when a default country is known at creation), but markets are optional — check store.has_markets?; currency and locale fall back to store-level defaults when no market exists. Orders are placed in a Market — that's what controls the currency the customer sees and what tax rules apply.
For full Market documentation see node_modules/@pallastrade/docs/dist/developer/core-concepts/markets.md.
Cart vs Order
In PallasTrade, PallasTrade::Order is both the in-progress cart and the completed transaction. The state column tracks which phase: cart, address, delivery, payment, confirm, complete. Filter on state to distinguish:
PallasTrade::Order.where(state: 'cart')
PallasTrade::Order.where(state: 'complete')
PallasTrade::Order.complete
Order#token (has_secure_token :token, length: 35) identifies an anonymous cart across requests. Logged-in carts are owned via the user_id FK.
Order parent/child + split_from (P1, 数据层)
P1(2026-08-26)为「父子单 / 拆单 / 合并支付」铺数据地基。以下关联已存在但尚未接入任何业务流程。
orders.parent_id(可空自引用 FK)→ Order#parent / #children(dependent: :nullify)。
- 语义方法:
parent_order?(有 children)/ child_order?(有 parent)/ single_order?(两者皆无,未拆单订单)/ sibling_orders / root_order(沿父链到根,防环)。
- 未拆单订单
parent_id = NULL,行为完全不变。
orders.split_from_id(可空 FK)→ Order#split_from / #split_orders:拆单来源血缘(展示用)。
orders.payment_combination_id(可空)→ 合并支付归属(跨父订单聚合支付)。
PaymentCombination / PaymentSplit:见 pallastrade-payments SKILL(P1 数据层)。
- 统一拆单引擎:
PallasTrade::Orders::Splitter(P2)——把订单按分组拆成子订单,迁移行项目/分摊调整/分摊已付 PaymentSplit/重算金额;策略 SplitStrategies::ByStockLocation / ByStore。详见 pallastrade-checkout SKILL。
Order 聚合派生 (P3, 只读派生)
P3(2026-08-27)为父订单(有 children)提供金额/支付/发货状态聚合。这些方法不覆写核心 total / payment_total / outstanding_balance / shipment_state——核心方法仍被 OrderUpdater / 状态机 / 校验依赖;聚合方法仅供序列化器 / 查询在父订单时使用,无 children 时回退原值(零行为变化)。
combined_total:own(item + shipment + adjustment)+ Σ children.combined_total(递归)。
combined_payment_total:own completed payments + Σ children。
combined_outstanding_balance:与 outstanding_balance 同规则(取消 → -payment;否则 total - (payment + reimbursement)),基于聚合值。
combined_amount_due:[combined_outstanding_balance - total_applied_store_credit, 0].max。
combined_shipment_state:聚合 own+children 状态,套 OrderUpdater#update_shipment_state 规则(backorder → backorder;多状态含 shipped → partial;含 pending → pending;否则 ready)。
combined_payment_state:基于 combined_outstanding_balance(>0 → balance_due;<0 → credit_owed;=0 → paid;取消且 0 → void)。
effective_payment_total:有 PaymentSplit 时用 captured - refunded(拆单记账分摊),否则 payment_total。
- 上述金额方法已注册
money_methods(display_combined_* 可用)。Store/Admin OrderSerializer 在 parent_order? 时用聚合值输出 total / amount_due / payment_status / fulfillment_status。
合并支付数据配套 (P4, 2026-08-27)
P4 实现 PaymentCombination 服务层时的数据/模型配套(payment_splits.payment_id 由 NOT NULL 改为可空,迁移 20260827000001):
payment_splits.payment_id 可空:PaymentCombinations::Create 在支付发生前建 split(payment 后补),Complete 回填。
Payment#order 改 optional: true:组合支付挂 order_id=nil;update_order / invalidate_old_payments / currency(order&.currency || payment_combination&.currency)已有 nil 守卫。
PaymentCombination#payments 关联(has_many :payments,组合支付本身,dependent: :nullify)。
OrderUpdater#update_payment_total:订单存在有效 PaymentSplit 时取 captured - refunded(组合/拆单成员订单的已付金额以 split 为准,因为组合 payment 不在 order.payments 里)。
- 服务层(Create/Complete/SettleJob/Webhook 分支)见
pallastrade-payments SKILL。
Checkout-side models
Order → Payment → PaymentMethod
Order → Shipment → ShippingRate → ShippingMethod
Order → Address (bill_address, ship_address)
- Payment has its own state machine (
checkout → processing → pending → completed, plus failed, void, and invalid). Column is state.
- Shipment has its own state machine (
pending → ready → shipped with canceled). Column is state.
- ShippingRate is a per-Shipment offer (e.g. UPS Ground $5.99, USPS Priority $8.99). The customer picks one.
Customer / User
PallasTrade.user_class (typically PallasTrade::User)
↓
Address (many, via pallastrade_addresses)
CreditCard (many)
GiftCard (many)
StoreCredit (many)
Use PallasTrade.user_class and PallasTrade.admin_user_class to reference user models — never PallasTrade::User directly. Apps can swap in their own user model via configuration.
用户黑名单(P8, 2026-08-28)
pallastrade_users.blacklisted_at(datetime,可空)——用户拉黑时间戳。由 PallasTrade::Risk::BlacklistRule 在下单前置校验时拦截(命中 → user_blacklisted 错误)。
Adjustments (polymorphic)
Adjustable (Order, LineItem, Shipment) ← Adjustment
Adjustment is polymorphic — it attaches to any Order, LineItem, or Shipment via adjustable_type + adjustable_id. Each Adjustment has a source (the thing that created it: a TaxRate, PromotionAction, ReturnAuthorization, etc.) and built-in scopes to filter by source type:
order.adjustments.tax
order.adjustments.promotion
order.adjustments.return_authorization
order.all_adjustments
Prefixed IDs
Every PallasTrade model exposed via the v3 API has a Stripe-style prefixed ID:
product.prefixed_id
order.prefixed_id
variant.prefixed_id
IDs are computed from the integer PK via Sqids — no database column. The prefix is declared per-class via has_prefix_id :<prefix> on the model. The v3 API accepts and emits prefixed IDs everywhere; find_by_prefix_id! resolves them back to integer PKs.
Conventions for the prefix:
- Long form for some resources:
prod (Product), variant (Variant)
- Short codes for most others:
or (Order), py (Payment, Stripe parity), adj (Adjustment), li (LineItem), ctg (Category/Taxon), cus (customer, Stripe parity), ch (Channel), mkt (Market)
Never expose raw integer PKs in API responses.
state vs status (mixed on 5.5)
Different models use different column names depending on when they were introduced:
Order.state, Payment.state, Shipment.state — older state machines
OrderApproval.status — newer status column
Channel doesn't use a state machine — it has an active boolean instead
When writing model code, follow the convention of the column the model actually has. When querying, check the model's source if you're not sure.
PallasTrade::Current (per-request context)
Avoid passing store / currency / locale around as arguments. Use the ambient context:
PallasTrade::Current.store
PallasTrade::Current.currency
PallasTrade::Current.locale
PallasTrade::Current.channel
PallasTrade::Current.market
Available in models, controllers, jobs, and services. Set automatically by controller before_actions on the API (with built-in fallbacks to store defaults inside PallasTrade::Current); you set it manually in jobs and rake tasks that need to address a specific store.
When to read further
- Field-level docs:
node_modules/@pallastrade/docs/dist/developer/core-concepts/<topic>.md for each model.
- OpenAPI spec:
node_modules/@pallastrade/docs/dist/api-reference/store.yaml lists every API field and its type — better than guessing from the model source.
- Adding new models / API resources: use the
pallastrade-resource skill.
- Extending existing PallasTrade models (add an association, validation, scope, method via decorator): use the
pallastrade-decorators skill.