Data model¶
Five tables hold imported data. Nine views sit on top of them and do the aggregation that the API exposes. Nothing is denormalised into a reporting table, which is why changing a cost price immediately changes every historical profit figure.
Tables¶
| Table | Holds |
|---|---|
products |
One row per product per marketplace: name, category, your cost price |
sales |
Completed sales with revenue, commission, logistics and net revenue |
orders |
Orders as reported by the marketplace, including those not yet delivered |
stocks |
Stock quantity per warehouse per date |
card_stats |
Daily listing statistics: impressions, clicks, cart additions, orders |
sales and orders are separate because a marketplace reports them at different points
in its own lifecycle. Financial figures throughout the product come from sales.
Why source and sku together¶
Every table keys products on the pair (source, sku) rather than on sku alone.
Wildberries and Ozon allocate SKUs independently, so the same number can identify unrelated products on the two platforms. Beyond avoiding that collision, keeping them separate is what makes per-platform comparison possible: commission structures, logistics costs and listing statistics differ enough that merging an item's two listings into one row would destroy the comparison the product exists to provide.
The practical consequence for a seller is that an item listed on both marketplaces has its cost price set twice, once per platform.
Views¶
The API reads almost exclusively from views. Each one corresponds to a section of the dashboard.
| View | Feeds |
|---|---|
v_profit_daily |
Daily, all-time and finance exports |
v_profit_weekly |
Weekly profit series |
v_profit_monthly |
Monthly profit series, including cost of goods |
v_profit_quarterly |
Quarterly profit series |
v_profitability |
Per-product economics and ROI |
v_abc_analysis |
ABC classification by cumulative profit share |
v_reorder_list |
Stock levels, days of cover and urgency status |
v_card_performance |
Daily listing statistics with derived rates |
v_platform_comparison |
Marketplace comparison with deductions as percentages |
Because these are views rather than materialised tables, they reflect the current state of the underlying data on every query. There is no refresh step and no staleness, at the cost of doing the aggregation work each time a request arrives.
How profit is calculated¶
Three figures are easy to conflate, and the difference between them matters.
Revenue is what the buyer paid.
Net revenue is revenue minus the marketplace's commission and logistics charges. It is what the marketplace actually transfers to you.
Gross profit is net revenue minus the cost of the units sold, where cost comes from
the cost_price you entered.
Products without a cost price contribute zero cost, so gross profit for them equals net
revenue. This is why the dashboard overstates profit until cost prices are filled in,
and why ROI is reported as null rather than as an inflated number.
Null semantics¶
Nulls in this data set are meaningful and are preserved deliberately rather than coalesced to zero.
| Field | null means |
|---|---|
cost_price |
Not entered yet, so profit for this product is unreliable |
roi_pct |
No cost price, so return on cost is undefined |
days_of_stock |
No recent sales, so there is no rate to project from |
revenue_wow, units_wow |
No preceding week, or the preceding week was zero |
*_delta |
The comparison period was zero |
category |
The marketplace supplied none |
Rendering any of these as 0 turns "unknown" into a specific and wrong claim. Client
code should show them as blank.
Time handling¶
Relative windows are computed from the server's local clock rather than from UTC. If the server's timezone differs from the one your marketplace account reports in, day boundaries shift accordingly, and a figure labelled "last 30 days" covers a window offset by that difference.
Setting the server to the same timezone as the seller account avoids the whole class of problem.