Skip to content
PHP2Shell: The PHP Bug
Akshay Thakare
Retail data engineering projectAdvanced

🚀 Walmart Data Engineering Project | End-to-End AWS & Snowflake Pipeline

An end-to-end retail pipeline built on synthetic data. Point-of-sale transactions in PostgreSQL and inventory in MongoDB are extracted by arrival time. Every store-day is reconciled against the store's close report, and daily sales and inventory KPIs are MERGEd into Snowflake. The local reference is verified by 12 passing tests; the AWS and Snowflake code is supplied but not yet run in the cloud.

Independent educational project with synthetic data. It is not Walmart’s real infrastructure, data or code, and is not sponsored by Walmart.

Browse the code Video walkthrough coming soon
  • Python
  • PySpark
  • PostgreSQL
  • MongoDB
  • Docker
  • AWS Glue
  • Amazon S3
  • AWS Lambda
  • DynamoDB
  • Snowflake
  • Step Functions
  • Apache Airflow
  • Amazon SNS
$217.50
Net sales (fixture)
5
Transactions
$43.50
Average basket
100%
Store-days reconciled
PostgreSQLMongoDB
AWS
GlueS3LambdaStep FunctionsDynamoDBSNS
Snowflake
Power BIplanned

Hand-checked fixture · 2 stores · 1 late transaction · asserted by tests

Key highlights

  • Extract by arrival timeA register that syncs the next morning is picked up by the next window — exactly once.
  • Store close reconciliationA store-day is published only when transactions and net sales match the store’s close report.
  • Late data holds the day, not the runOther stores publish; the incomplete store-day waits as INCOMPLETE.
  • Resends replace, never addLines are keyed on (txn_id, line_no); the latest copy wins.
  • MERGE at fact grainRe-running a window rewrites identical rows in Snowflake.
  • Scales with a knobThe generator produces 42,000 transactions (144k lines) locally in about 22 seconds.

Project status

TrackCode writtenTested locallyRun in AWS

Local reference pipeline

Python stdlib + SQLite, every business rule

—

AWS & Snowflake code

Glue, Lambda, DynamoDB, Step Functions, Airflow, SNS

Power BI report

KPI views and DAX written, data export tested; the .pbix is not built and the DAX has not been run

12 passing tests · 13 components · 1 planned

Important: the cloud code has not been run in an AWS or Snowflake account yet, and the repository is not public yet. Everything marked “tested locally” is proven by the Python + SQLite reference.

1

Architecture

Switch views, press play to follow one batch, click a component to see its code.

Click a component for details and code. Dots: green tested locally · blue code supplied · amber planned.

Store systems

AWS pipeline

Warehouse & BI

Orchestration & control plane

Glue Extract

Ingestion

Reads rows whose ingested_at falls in the window, with the predicate pushed down to PostgreSQL, plus inventory from MongoDB and snapshots of the small dimensions. Writes Parquet partitioned by business_date and a manifest with row counts and file ETags.

  • Code
  • Tested locally
  • Cloud run — not yet

PostgreSQL POS to Glue Extract: JDBC window. MongoDB to Glue Extract: connector. Glue Extract to S3 Raw: Parquet + manifest. S3 Raw to Validate Batch: size + ETag. Validate Batch to PySpark Reconcile: validated. PySpark Reconcile to S3 Curated & Publish: facts + status. S3 Curated & Publish to Snowflake: COPY + MERGE. Snowflake to Power BI Report: KPI views. Airflow to Step Functions: start + wait. Step Functions to DynamoDB Run Ledger: claim / mark. Step Functions to SNS Alerts: on failure.

2

Read the case study

Every design decision, chapter by chapter — start here before the interview questions.

ch. 011 / 10

Overview & the retail problem

The problem

A large retailer's registers send transactions all day. Most reach the central database within minutes. Some don't: a register that lost its connection syncs the next morning, and the point-of-sale (POS) system sometimes sends the same line twice.

Store managers still need daily sales they can trust. Those sales must be:

  • complete: every transaction rung up that day, including the late ones
  • not double-counted: a resend must never add sales
  • reconciled: equal to the store's own end-of-day close report

This is an independent educational project with synthetic data. It is not Walmart's real infrastructure, data or code, and it is not sponsored by Walmart.

What the pipeline does

  1. Extract every row that arrived in a window, from PostgreSQL (POS) and MongoDB (inventory and promotions).
  2. Validate the raw files against a manifest.
  3. Merge them into curated history. A resend replaces the earlier copy.
  4. Reconcile every store-day against the store close report.
  5. Publish only the store-days that match. Facts are MERGEd into Snowflake at their grain.
  6. Show sales, basket, department and stockout KPIs on a dashboard.

The baseline you can check by hand

TxnStoreLinesNetArrives
T11001 East4 × $2.50 + 1 × $100.00$110.00same day
T21001 East2 × $4.00 (line resent once)$8.00same day
T31002 West$100.00 − $10.00 promotion$90.00same day
T41002 Westreturn 1 × $2.50−$2.50same day
T51002 West3 × $4.00$12.00next morning

Window 1: store 1001 publishes $118.00. Store 1002 has 2 of its 3 transactions, so it stays INCOMPLETE.

Window 2: T5 arrives and store 1002 publishes $99.50.

Final result: $217.50 net sales · 5 transactions · 10 units · $43.50 average basket · 2.00 units per transaction. Re-running either window changes nothing.

ch. 022 / 10

Architecture & why each service

LayerServiceWhy it is here
SourcesPostgreSQL (POS), MongoDB (inventory)Two real source shapes: relational transactions and document snapshots
IngestionAWS GlueJDBC and MongoDB connectors; the window predicate is pushed down to PostgreSQL
Raw storageAmazon S3Parquet partitioned by business_date, and a manifest per run
ValidationAWS LambdaFast checks of the manifest against S3 before Spark starts
Run ledgerDynamoDBA conditional put stops the same window from being processed twice
TransformationGlue PySparkMerge into curated history, reconcile store-days, aggregate facts
WarehouseSnowflakeCOPY INTO staging, then MERGE in one transaction
OrchestrationAirflow → Step FunctionsAirflow decides when; Step Functions decides how
AlertingAmazon SNSEvery failed run sends its run id, window and error

Data plane and control plane

Files move through S3: raw/ → curated/ → publish/. Step Functions never carries data, only the run_id and the window.

The run id is a hash of the window, so a retried window maps to the same raw prefix and the same ledger item.

What is verified

The local reference (retail/) implements every rule in Python and SQLite and is covered by 12 tests. The Glue, Lambda, Snowflake, Step Functions, Airflow and CloudFormation code mirrors it but has not been run in a cloud account yet.

ch. 033 / 10

Source systems & synthetic data

POS schema (PostgreSQL)

stores, departments, products, transactions, transaction_lines and store_close. Every fact table has an ingested_at column and an index on it, because extraction filters by arrival time.

transaction_lines deliberately has no primary key. Real POS feeds resend lines, so the pipeline must be able to deduplicate them.

Inventory (MongoDB)

inventory_snapshots holds one document per store, SKU and day, with on_hand and on_order. promotions holds SKU lists with start and end dates and a discount.

The generator

retail/generate.py is deterministic: the same seed always produces the same rows.

KnobDefaultExample at scale
--stores450
--days330
--txns (per store per day)40400

It models a few retail patterns:

  • Shoppers buy cheap staples far more often than electronics.
  • Promotions lift basket quantity.
  • About 3% of transactions are returns.
  • About 2% of transactions sync the next morning.
  • About 4% of shelves are empty at close.

Measured locally: 20 stores × 7 days × 300 transactions produces 42,000 transactions and 144,102 lines, processed end to end in about 22 seconds.

Shell.sh
python3 -m retail.generate --stores 50 --days 30 --txns 400 --out data/generated
python scripts/load_sources.py --data data/generated     # into real PostgreSQL + MongoDB

ch. 044 / 10

Incremental extract & raw validation

Extract by arrival time

Each run covers a half-open window [window_start, window_end) on ingested_at. The dimension tables (stores, departments, products) are small, so they are snapshotted in full on every run.

Why not filter by business date? Suppose a sale rung up on Monday reaches the database on Tuesday. If Monday was already extracted, that sale would be missed forever. Arrival windows pick up every row exactly once, and the transform maps it back to its business date.

The run is refused when a window does not start at the stored watermark, so no gap can silently skip rows.

The raw manifest

Plain text.txt
raw/run_id=<sha256(window)[:16]>/
  table=transactions/part-*.parquet
  table=transaction_lines/…
  manifest.json      # rows, bytes and ETag (cloud) or SHA-256 (local) per table

Validation (Lambda)

The validator checks:

  • the manifest window matches the execution input
  • all eight expected tables are present
  • every file is inside the run prefix, and its size and ETag are unchanged
  • required fields are present (checked locally)

It then claims the run in DynamoDB with attribute_not_exists(run_id) OR status = FAILED. A window that is in progress or has succeeded cannot be claimed again.

Two tests cover this: test_corrupted_raw_file_fails_validation and test_window_must_start_at_watermark.

ch. 055 / 10

Reconciliation, late data & transformation

Curate

RuleImplementation
Resends replaceUpsert on (txn_id, line_no); the latest ingested_at wins
Bad lines are rejectedUnknown SKU, zero quantity or non-positive price go to rejected_lines
A correction clears the rejectionAn accepted resend deletes the earlier rejection
ReturnsNegative quantity, tracked separately as return_cents
PromotionsLine discount_cents, tracked separately

Reconcile every store-day

For every store-day this run touched, curated totals are compared with the store close report:

ConditionStatusEffect
No close report yetOPENNothing published
Count and net match, no rejected linesPUBLISHEDStore-day MERGEd into the facts
Anything elseINCOMPLETEPreviously published facts stay; the day waits

One store's late register never blocks the other stores.

Facts

  • FACT_SALES_DAILY has one row per date × store × department, with units, gross, discount, returns and net.
  • FACT_STORE_DAILY has one row per date × store, with transactions, units and net.
  • FACT_INVENTORY_DAILY has one row per date × store × SKU, with on hand, on order and a stockout flag.

In the cloud, the Glue job reads curated history only for the touched business_date partitions. It materialises the merge with localCheckpoint() before overwriting those partitions, because Spark cannot safely overwrite a path it is still lazily reading.

ch. 066 / 10

Snowflake COPY INTO & MERGE

02_load_merge.sql runs per run id:

  1. COPY INTO four temporary staging tables from @PUBLISH_STAGE/run_id=…/.
  2. BEGIN.
  3. Delete and re-insert FACT_SALES_DAILY rows for the republished store-days. A department that dropped to zero must disappear, which a plain MERGE would not do.
  4. MERGE FACT_STORE_DAILY, FACT_INVENTORY_DAILY, STORE_DAY_STATUS and PIPELINE_RUNS.
  5. COMMIT.

If anything fails before COMMIT, everything rolls back and the facts keep their previous values. This is single-database atomicity, not distributed exactly-once.

The publisher only accepts a run id of 16 hex characters before substituting it into the SQL.

Dashboard views

ViewShows
KPI_DAILYNet sales, transactions, units, average basket, units per transaction
SALES_BY_REGIONNet sales by region and store format
SALES_BY_DEPARTMENTNet, returns and discounts by department
STOCKOUT_RATEShare of store-SKU snapshots at zero
STORE_DAYS_PENDINGStore-days waiting, with missing transactions and missing sales

ch. 077 / 10

Orchestration & alerting

Airflow: when

walmart_retail_daily runs @daily with catchup=True, so a backfill replays the days one by one. max_active_runs=1 keeps the windows in order. The DAG computes the window and the run id, starts Step Functions, and waits for it in reschedule mode.

Step Functions: how

Plain text.txt
ExtractRaw → ValidateBatch → TransformCurated → PublishSnowflake → MarkSucceeded
     ↘            ↘                 ↘                   ↘
                         MarkFailed → NotifyFailure (SNS) → Failed
  • ExtractRaw retries Glue service errors twice.
  • Every task's Catch routes to MarkFailed, which records the run in DynamoDB, then to NotifyFailure, which publishes run id, window and error.

What is not a failure

A store-day waiting for late data is normal operation. It shows up in STORE_DAYS_PENDING and does not page anyone. A freshness alert for days that stay pending too long is the next step.

ch. 088 / 10

Run it yourself

Local — no cloud account

Shell.sh
python3 -m unittest discover -s tests -t . -v      # 12 tests
python3 -m retail --root .runtime demo             # 4 daily windows, then KPIs

Expected output: the early windows report some incomplete store-days. After the last window, every store-day is PUBLISHED.

The 12 tests

TestProves
late transaction holds store-dayINCOMPLETE until T5 lands, then $217.50
resent line counted onceStore 1001 stays at 7 units and $118.00
returns and discountsGross, discount, returns and net per department
replaying a windowALREADY_PROCESSED; facts identical
window must start at watermarkNo gaps
failure before commitFacts and watermark roll back; a retry succeeds
corrupted raw fileChecksum and missing-table detection
rejected lineHolds the day until a corrected resend
stockout rate1 of 4 snapshots at zero = 0.2500
concurrent writerA second writer is refused
generator deterministicSame seed, same rows
generated data reconcilesEvery store-day publishes and totals equal the close reports

Cloud

Follow docs/STEPS.md in the repository, steps 3–9: Docker sources, CloudFormation, Glue jobs, Snowflake setup, the state machine and the Airflow DAG. Each step ends with a check you can see.

ch. 099 / 10

Verified vs planned & next steps

ComponentState
Generator, sources, extract, validation, reconciliation, MERGE, KPIsTested locally (12 tests)
PostgreSQL schema, Docker Compose, source loaderWritten; schema mirrors the local tables
Glue extract and transform, validation Lambda, Snowflake publisherCode supplied, not run in AWS
Snowflake setup, MERGE and viewsWritten, not run against Snowflake
Step Functions, Airflow, CloudFormationWritten, not deployed
Dashboard data export and previewTested locally (export: 9 tests; preview calculation rules: 22 tests)
Power BI report (.pbix), DAX measuresKPI views and powerbi/measures.dax written; not built, DAX not run in Power BI Desktop
RepositoryLocal only, not public yet

Next steps

  1. Run the whole chain in a real account. Capture Glue run ids, Snowflake query ids and the reconciled KPIs as evidence.
  2. Build the Power BI report from powerbi/README.md and pass its validation checklist.
  3. Load test at 50 stores × 30 days. Record Glue timings and cost.
  4. Add a freshness alert for store-days that stay INCOMPLETE past a deadline.
  5. Publish the repository.

ch. 1010 / 10

Power BI dashboard & data usage

Why a dashboard

The pipeline's job ends with published Snowflake tables. People only get value when they can read them. The report on top of those tables answers six kinds of questions, from six kinds of people:

WhoAsksPage
Store managerDid my store hit its numbers, and is yesterday final?Stores
MerchandiserWhich departments carry sales, and what do discounts and returns do to net?Executive overview
Regional managerWhich region is ahead, and which stores drag it down?Executive overview, then Stores
Supply chainWhich stores are running out of stock?Inventory & data quality
Finance analystCan I trust this number before I put it in a deck?Inventory & data quality
Data engineerIs the pipeline delivering complete data?Inventory & data quality

The interactive preview at the end of this page lets you try each of these questions with real pipeline output.

What is behind each number

VisualReadsMeasure
Net sales, sales by day, department and regionFACT_SALES_DAILY (date × store × department)Net Sales
Transactions, average basket, units per transactionFACT_STORE_DAILY (date × store)Transactions, Average Basket, Units per Transaction
Stockout rateFACT_INVENTORY_DAILY (date × store × SKU)Stockout Rate
Data completeness, pending store-daysSTORE_DAY_STATUS (date × store)Data Completeness, Pending Store-Days

Every measure is in powerbi/measures.dax and mirrors the Python and SQL logic that the 21 local tests cover, so the report reproduces the pipeline's numbers.

The rule that makes the numbers trustworthy

A store-day reaches the sales facts only when its totals match the store's own close report. Until then it is absent, not zero. The dashboard says so in three places:

  1. Data completeness is the share of store-days that are published.
  2. The latest day is marked provisional while any of its store-days are pending.
  3. A table lists each pending store-day with the transactions that have arrived against what the close report expects, how much of that day's sales is not yet published, and the late shortfall the pipeline is still waiting for.

In the demo data, after the last daily window 12 store-days are pending: for example one store has 148 of its 150 transactions in, and $11,420.60 of $11,600.10. After the catch-up window every store-day matches and net sales rise by exactly the close-report total of the store-days that were pending. A test asserts that equality.

Two modelling rules worth knowing

  • A basket cannot be split by department. One transaction contains items from many departments. When a department is selected, Transactions, Average Basket and Units per Transaction return blank, because showing a number would count the same basket once per department.
  • Net = gross − discounts − returns. Returns are negative quantities; discounts are stored on the line. Discount Rate and Return Rate explain the gap between gross and net.

Build it

  1. Follow powerbi/README.md: connect to Snowflake, load the published tables (never the raw or curated layers), mark a date table, create the relationships.
  2. Paste the measures from powerbi/measures.dax.
  3. Build the three pages from the table in the guide.
  4. Run the validation checklist before sharing the report. It compares the report with RETAIL.MART.KPI_DAILY, with the sum of the stores' close reports, and checks that a department selection blanks the basket measures.

To see the target before building, generate the sample data used by the preview: python scripts/export_dashboard_data.py.

What is verified and what is not

PartState
Dashboard data export (retail/dashboard.py)Tested locally: 9 tests, including reconciliation with the close reports
Preview on this pageCalculation rules tested against the exported data (22 tests); the DAX text it shows is checked against measures.dax
DAX measuresWritten to match the tested logic; not yet run in Power BI Desktop
Power BI file (.pbix) and scheduled refreshNot built
3

Interview questions

Project-based questions, easy to hard. Think first, then reveal the answer.

13 questions · think first, then reveal

Scenario questions: what breaks, and how it recovers

Register syncs the next morning

01
Symptom:
A transaction’s ingested_at is a day after its business date.
Impact:
Day-one sales understated.
Solution:
Extract by arrival time; the store-day waits as INCOMPLETE until totals match.

at ExtractRaw

Tested locally

POS resends a line

02
Symptom:
The same (txn_id, line_no) arrives twice.
Impact:
Double-counted units and sales.
Solution:
Upsert on (txn_id, line_no); the latest copy replaces the earlier one.

at TransformCurated

Tested locally

Unknown SKU on a line

03
Symptom:
A line references a product not in the catalog.
Impact:
Sales without a department.
Solution:
Reject the line, hold the store-day; a corrected resend clears it.

at TransformCurated

Tested locally

Returns and promotions

04
Symptom:
Negative quantities and line discounts.
Impact:
Gross vs net confusion.
Solution:
Track gross, discount, returns and net separately at fact grain.

at TransformCurated

Tested locally

Window re-run

05
Symptom:
Airflow retries a window that already succeeded.
Impact:
Duplicate facts if appended.
Solution:
Run ledger returns ALREADY_PROCESSED; facts MERGE at grain anyway.

at ValidateBatch

Tested locally

Gap between windows

06
Symptom:
A window starts after the watermark.
Impact:
Rows silently skipped.
Solution:
Refuse any window that does not start at the watermark.

at Pipeline start

Tested locally

Raw file changed after extract

07
Symptom:
Checksum or ETag differs from the manifest.
Impact:
Unknown data reaches facts.
Solution:
Fail validation; re-extract the window.

at ValidateBatch

Tested locally

Failure before commit

08
Symptom:
Load fails half-way.
Impact:
Half-updated facts.
Solution:
One transaction: facts, statuses, run log and watermark roll back together.

at PublishSnowflake

Tested locally

Two runs at once

09
Symptom:
A manual trigger overlaps the schedule.
Impact:
Interleaved writes.
Solution:
DynamoDB conditional claim in the cloud; BEGIN IMMEDIATE locally.

at ValidateBatch / publish

Cloud code

Source schema change

10
Symptom:
POS adds or renames a column.
Impact:
Failed job or silent nulls.
Solution:
Required-field contract fails the run; migrate the schema deliberately.

at ExtractRaw

Partial
4

Run & read the code

Watch one execution, follow the records, then open the exact files.

Step Functions: one ingestion window

Airflow starts one execution per daily window and waits. Every task failure routes to the same path: mark the run FAILED, then alert.

STEP 01 · AWS Glue

ExtractRaw

Read rows that arrived in the window from PostgreSQL and MongoDB; write Parquet and a manifest.

Input
run_id, window_start, window_end
Output
raw/run_id=…/table=…/ + manifest.json
Next
ValidateBatch
On failure
Retried twice for Glue service errors, then fails the run. → MarkFailed

Failure handling — every failure is recorded and announced

  • Extract fails

    Glue service errors retry twice; anything else fails the run.

    → MarkFailed → NotifyFailure

  • Manifest does not match S3

    Stop before transforming; re-run the window to re-extract.

    → MarkFailed → NotifyFailure

  • Same window triggered twice

    The DynamoDB conditional write refuses the second claim.

    → MarkFailed → NotifyFailure

  • Snowflake load fails

    Transaction rolls back; facts keep their previous values.

    → MarkFailed → NotifyFailure

Store-days waiting for late data are not failures: they stay INCOMPLETE and appear in the STORE_DAYS_PENDING view.

Code explorer — VS Code editor, snapshot of the local repository

walmart-retail-pipeline — Visual Studio Code

retail › pipeline.py — Curate → reconcile → MERGE publish, one transaction

Fetching retail/pipeline.py from GitHub…
mainsnapshot of the local repositorypythonread-onlyrepository not public yet

Data flow, record by record

A register sale becomes a row in FACT_SALES_DAILY (store × department × day) and FACT_STORE_DAILY.

  1. 1. Source

    PostgreSQL POS

    • transactions
    • transaction_lines
    • store_close
  2. 2. Raw S3

    Parquet

    • raw/run_id=…/
    • table=…/
    • manifest.json
  3. 3. Validation

    Lambda

    • 8 tables present
    • Size + ETag per file
    • Window matches
  4. 4. Curate

    PySpark

    • Reject unknown SKU
    • Resend replaces line
    • Returns as −qty
  5. 5. Reconcile

    Store close

    • Count matches?
    • Net sales matches?
    • PUBLISHED / INCOMPLETE
  6. 6. Snowflake

    MERGE

    • FACT_SALES_DAILY
    • FACT_STORE_DAILY
    • One COMMIT

Sample output — the fixture behind $217.50 · 5 transactions · $43.50 basket

tests/fixture.py · business date 2026-01-05
txn_idstorelinesnetarrives
T11001 East4 × $2.50 + 1 × $100.00$110.00same day
T21001 East2 × $4.00 (line resent once)$8.00same day
T31002 West1 × $100.00 − $10.00 promo$90.00same day
T41002 Westreturn 1 × $2.50−$2.50same day
T51002 West3 × $4.00$12.00next morning

Why not publish $205.50 on day one?: Without reconciliation, window 1 would report store 1002 at $87.50 and the dashboard would be $12.00 short until someone noticed. Holding the store-day as INCOMPLETE keeps every published number equal to what the store actually rang up.

5

Learning outcomes & next steps

  • Incremental ingestion
  • Reconciliation & late data
  • Retail data modeling
  • Idempotent MERGE loads
  • Orchestration & alerting
  • Synthetic data at scale

Future enhancements

  • Run the full chain in a real AWS and Snowflake account and capture query IDs
  • Build the Power BI report from powerbi/README.md and pass its validation checklist
  • Publish the repository on GitHub
  • Load test with 50 stores × 30 days and record Glue timings and cost
  • Freshness alert when a store-day stays INCOMPLETE past a deadline

Each step ships with evidence — logs, query IDs, reconciled metrics — not just code.

6

Power BI dashboard: how people use this data

A pipeline is only useful if people can read what it produces. This is the report that sits on top of the published Snowflake tables: three pages, one for each kind of question. Try it: pick a region or a department, click a store, switch to “Provisional” to see what the report shows while late registers are still syncing, and open “How this is built” under any visual to see its source table and DAX measure.

This is an interactive replica of the report, not a screenshot. It is drawn from real output of the pipeline’s local reference run (synthetic data: 12 stores, 14 days, 25,200 transactions) using the same calculation rules as powerbi/measures.dax, and those rules are covered by tests. The Power BI file itself (.pbix) has not been built yet, and the DAX has not been run in Power BI Desktop.

Loading report data…

Who uses it, and for what

  • Store manager

    Did my store hit its numbers, and is yesterday final?

    page: Stores

  • Merchandiser

    Which departments carry sales, and what do discounts and returns do to net?

    page: Executive overview

  • Regional manager

    Which region is ahead, and which stores drag it down?

    page: Executive overview, then Stores

  • Supply chain

    Which stores are running out of stock?

    page: Inventory & data quality

  • Finance analyst

    Can I trust this number before I put it in a deck?

    page: Inventory & data quality

  • Data engineer

    Is the pipeline delivering complete data?

    page: Inventory & data quality

Read it correctly

  • The latest day is provisional until its late registers sync. A pending store-day is missing from the sales entirely, not shown as zero. Check Data completeness first.
  • Selecting a department blanks Transactions, Average basket and Units per transaction: one basket holds items from many departments.
  • Net sales = gross − discounts − returns. Returns are negative quantities.
  • Stockout rate counts store-SKU snapshots with nothing on hand. It is not an estimate of lost sales.

Build it yourself