Profile

Data governancepack

Lineage · Data dictionary · Access matrix · Data-quality rules · by Jidapa Pooljan

Project · Data governance

How the sales and stock data is governed, from source file to dashboard.

These are the four documents that sit behind the dashboards in my portfolio: where each number comes from, what each column means, who can see what, and which checks run before data is loaded. They describe an FMCG importer's sales and stock data in PostgreSQL, fed by a Python ETL and published as encrypted HTML dashboards.

Built on the same mock dataset as the demo dashboards (companies ALPHA, Beta, Northwind, Bluepeak; fake brands and customers). Table names are simplified. The roles, rules and checks follow the ones I run at work.
4 → 1

source systems feeding one PostgreSQL database

6

datasets registered, each with an owner and a sensitivity class

6 × 6

roles × dashboards in the access matrix

10

data-quality checks, 4 of which block bad data from loading

01 · Data lineage

Source to dashboard

Every dashboard number can be traced back to one source file. Data moves left to right only; nothing is typed in by hand after the source.

Sources

ERP sales exportInvoice lines, monthly
WMS stock exportLots by warehouse, daily
Supplier & mall portalsPulled by Selenium on a schedule
Product & customer mastersMaintained by sales ops

Ingest & validate

Python ETLOne typed transform for every source
Quality checks10 rules: block, warn or fix (section 04)
Rejected rowsLogged with the reason, fixed at source

PostgreSQL

stagingRaw rows as received, typed
coresales_fact stock_lot + masters
martMonthly, customer, product and salesperson roll-ups

Build & protect

Python buildQueries the mart, checks the totals agree
AES-GCM per roleData encrypted inside each HTML file
Static hostingNo server, no open database port

Consumers

DashboardsSales, Exec, Stock, Stock Value, Mall, Biz
Excel & Power BIRead-only ODBC account
Auditors & vendorsReconciled extracts on request

Metric lineage

Dashboard metricComputed asComes fromGuarded by
Net sales (THB)SUM(net_sales)ERP invoice amount after discount, excl. VAT → core.sales_factQ2, Q3, Q7, Q9
Units in cartonsunit_pieces ÷ pack_sizeERP quantity in pieces + product master pack sizeQ4
Stock statusShare of shelf life left: Expired / Critical <60% / Warning <70% / SafeWMS mfg_date, exp_date → core.stock_lotQ5, Q10
Cover daysStock on hand (cartons) ÷ average daily sales, Grade A onlycore.stock_lot + average of the last 6 months with sales in core.sales_factQ2, Q8

02 · Data dictionary

What each table and column means

The register lists every dataset with its grain, refresh, owner and sensitivity. Below it are the column definitions for the two core tables the dashboards read.

Dataset register

DatasetOne row perSourceRefreshBusiness ownerClass
core.sales_factCustomer × product × monthERP sales exportMonthlySales operationsInternal
core.stock_lotCompany × product × lotWMS stock exportDailyWarehouseInternal
core.product_masterProduct codeProduct master fileOn changeBrand / category teamInternal
core.customer_masterCustomerERP customer listOn changeSales operationsInternal
core.landed_costProduct × shipmentImport costingPer shipmentFinanceRestricted
mart.salesperson_monthSalesperson × monthRoll-up of sales_factMonthlySales managementRestricted
Internalstaff roles that need it Restrictedadmin and executive only: cost, margin, individual performance Data steward for every dataset: Data Analyst (me)
Grain: customer × product × monthKey: year_col, month_col, customer_name1, product_codeColumns: 16
ColumnTypeMeaningRule / exampleClass
year_colintSales year2024 … current yearInternal
month_colintSales month1–12; partial current month is excludedInternal
customer_name1textCustomer trading nameMust exist in customer master (Q3)Internal
channeltextSales channel of the customerModern Trade, Traditional Trade, Online, Other (Q6)Internal
product_codetextProduct codeStored as text so leading zeros survive: 0001244, not 1244 (Q1)Internal
product_nametextProduct descriptionFrom product master, not from the invoiceInternal
brandtextBrande.g. BRIGHTLY, PURELYInternal
actual_companytextCompany that owns the brandALPHA or BetaInternal
group_categorytextTop-level product groupFMCG, Pet, Kitchen (Q6)Internal
categorytextProduct categoryHome Care, Personal Care, Tissue…Internal
sub_cat_cleaned_v1textSub-category after clean-up mapping v1Merges spelling variants, e.g. “Dishwash” → Dish WashInternal
pack_sizeintPieces per carton> 0, from product master (Q4)Internal
unit_piecesnumericQuantity sold in pieces≥ 0 except credit notesInternal
unit_packnumericQuantity sold in cartons= unit_pieces ÷ pack_sizeInternal
net_salesnumeric(14,2)Sales after discount, excl. VAT, THBTotals must agree across every mart table (Q9)Internal
saler_name, person_keytextSalesperson and their codeR01…; links sales to individual performanceRestricted

03 · Access matrix

Least privilege, by role

Each role has one password. It unlocks only the dashboards granted to that role; the others stay encrypted in the browser. Cost, margin and individual sales performance are limited to admin and executive.

Dashboards by role

RoleWhoStock AgingSale-InMall Sale-OutBusinessProfit PulseStock Value
adminData team✓✓✓✓✓✓
execExecutives✓✓✓✓✓✓
sale2mtSales lead, modern trade✓✓✓–––
sale2ttSales lead, traditional trade✓✓––––
sale1Field sales✓–––––
pcareCategory team✓–––––

Database accounts

AccountUsed byCan readCan write
etl_writerThe Python ETL onlystaging, corestaging, core (no DROP or ALTER)
report_readerExcel and Power BI over ODBCmart, without Restricted tablesNo, read-only
exec_readerDashboard build for admin and execmart, including Restricted tablesNo, read-only
ownerSchema changes and backupsEverythingEverything; not used day to day

How it is enforced

Encrypted at rest in the file

Each dashboard's data is encrypted with AES-GCM. A role's password unwraps only the keys for its granted dashboards, so sharing the file does not share the data.

Nobody edits from Excel

Spreadsheets and Power BI connect with a read-only account. Only the ETL can write, so a wrong paste cannot change the source of truth.

Sensitive figures kept apart

Landed cost and per-salesperson results sit in Restricted tables that only the admin and exec builds read.

04 · Data-quality rules

Checked before data reaches the database

Every source goes through the same checks. A check either blocks the row or batch, warns and loads, or fixes the value in a known, logged way.

#CheckStageWhat it catchesAction
Q1Product codes kept as textLoadExcel turning 0001244 into 1244, which attached sales to the wrong SKUFix
Q2Product code exists in product masterValidateNew or mistyped codes; one batch had 14 missing codesBlock row
Q3Customer is mappedValidateRows that used to load as null and drop out of revenue totalsBlock row
Q4Pack size present and > 0ValidateCartons and net sales computed as zeroWarn
Q5One date formatLoad1/12/25, 2025-12-01 and 12/1/25 in the same file, which made phantom gaps in trendsFix to YYYY-MM-DD
Q6Values in the reference listValidateChannel, category or sub-category names drifting between source periodsWarn
Q7No duplicate rows on the table keyValidateThe same month loaded twice, doubling salesBlock batch
Q8Stock quantities add upValidateNegative quantities, or good + damage + wip ≠ totalWarn
Q9Totals agree across tablesBuildMonthly, customer, product and salesperson tables that differ beyond roundingStop build
Q10Expiry status ruleBuildExpired if past expiry; Critical if <60% of shelf life left; Warning <70%; no manufacture date falls back to 30 / 90 daysClassify
Blockrow or batch does not load Warnloads and is listed in the run log Fixchanged by a fixed, documented rule

Old records were cleaned the same way: SKUs that appeared under two names were merged so year-over-year trends compare like with like, and the database is backed up on a schedule.

See the dashboards these rules protect

Four live demos, with the full background in my profile.