Skip to content
55 changes: 55 additions & 0 deletions inventory-reconciliation/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Inventory Reconciliation – Notes

## Approach

I started by pulling both CSVs and reading them in full before writing any code. The first thing that jumped out was that the two files use **different column names** (`name` vs `product_name`, `quantity` vs `qty`, etc.), so a naive join on column position would silently mangle the data. I built a `COLUMN_MAP` alias layer to normalise headers before any processing, making the reconciliation logic itself header-agnostic.

The reconciliation logic is a straightforward set difference + intersection:
- **Both snapshots** → classify as `unchanged` or `quantity_changed` (and flag name changes separately)
- **Snapshot 1 only** → `removed`
- **Snapshot 2 only** → `added`

All data quality issues are collected as typed records (`DataQualityIssue`) and surfaced in the JSON report alongside the reconciliation results, rather than silently dropped or corrected without trace.

## Data Quality Issues Found

| Issue | Count | Example |
|---|---|---|
| Non-standard SKU format (missing dash) | 2 | `SKU005` → `SKU-005`, `SKU018` → `SKU-018` |
| Lowercase SKU | 1 | `sku-008` → `SKU-008` |
| Leading/trailing whitespace in name | 5 | `" Widget B"`, `"Mounting Bracket Large "`, `" HDMI Cable 3ft "`, `" Compressed Air Can"` |
| Float quantity instead of integer | 2 | `70.0`, `80.00` |
| Negative quantity | 1 | SKU-045 has qty `-5` (second duplicate row) |
| Non-ISO date format | 1 | `01/15/2024` on SKU-035 in snapshot 2 |
| Duplicate SKU | 1 | SKU-045 appears twice in snapshot 2 with different names and quantities |
| Name discrepancy (same SKU, different name) | 1 | `Multimeter Pro` → `Multimeter Professional` |

The duplicate SKU-045 was the most notable issue: snapshot 2 contains it once as `"Multimeter Professional"` (qty 23) and again as `"Multimeter Pro"` (qty -5). I keep the first occurrence, flag both problems (duplicate + negative quantity), and surface both in the quality report. This is a judgment call — in production I'd want to confirm the intended resolution with the data owner.

## Key Decisions

**SKU as join key** — SKUs are the only stable identifier across both snapshots; product names drift (see Multimeter Pro/Professional). Normalising SKUs (uppercase, insert dash where missing) before joining prevents false "removed/added" mismatches for what are clearly the same item.

**Collect-don't-throw on quality issues** — Rather than raising exceptions on bad data, every issue is recorded with its raw and resolved value. This gives the downstream consumer a complete picture and makes the reconciliation idempotent.

**First-row-wins on duplicate SKUs** — Without domain knowledge about which duplicate is correct, keeping the first occurrence is the safest default. The issue is logged so nothing is silently discarded.

**Name changes are advisory, not status-changing** — A name change on an existing SKU doesn't change `status`; it's flagged separately as `name_changed: true` with both names. Renaming a product shouldn't look like a removal + addition.

## AI Tooling

I used Claude throughout this exercise, which is consistent with how I work day-to-day. Specifically:
- **Data inspection**: asked Claude to scan both CSVs and enumerate all quality issues before writing a line of code — this produced the complete issue catalogue above in one pass.
- **Scaffolding**: used Claude to generate the initial dataclass model and column-alias approach, then reviewed and adjusted the normalisation edge cases.
- **Test generation**: Claude drafted the pytest suite from the module's public interface; I reviewed each test case for correctness and added the edge-case tests for `None` quantity delta and result ordering.
- **Bug catching**: Claude caught the `70.0 == 70` Python equality pitfall (float detection checking value rather than raw string) before I ran the tests.

The commit history reflects this iterative, AI-assisted flow: data inspection → structure → implementation → tests → fix.

I also flag warehouse location changes separately from quantity changes — a SKU can be "unchanged" in quantity but physically transferred between warehouses, which is meaningful to operations teams.

Items with zero quantity are flagged as out_of_stock — they exist in the system but are operationally unavailable. This is distinct from removed (not in snapshot at all).

Quantity direction (increased/decreased) is tracked separately from the status — a restock and a consumption both show as `quantity_changed` but mean very different things operationally.

Warehouse location changes are flagged independently — an item can be `unchanged` in quantity but physically transferred between warehouses, which is a meaningful operational event.
81 changes: 81 additions & 0 deletions inventory-reconciliation/output/reconciliation_report.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
sku,name,status,snap1_qty,snap2_qty,qty_delta,location,name_changed,snap1_name,snap2_name
SKU-001,Widget A,quantity_changed,150.0,145.0,-5.0,Warehouse A,False,,
SKU-002,Widget B,quantity_changed,75.0,70.0,-5.0,Warehouse A,False,,
SKU-003,Gadget Pro,quantity_changed,200.0,185.0,-15.0,Warehouse B,False,,
SKU-004,Gadget Lite,quantity_changed,50.0,48.0,-2.0,Warehouse A,False,,
SKU-005,Connector Cable 6ft,quantity_changed,500.0,480.0,-20.0,Warehouse C,False,,
SKU-006,Connector Cable 10ft,unchanged,350.0,350.0,0.0,Warehouse C,False,,
SKU-007,Power Supply Unit,unchanged,80.0,80.0,0.0,Warehouse A,False,,
SKU-008,Power Supply Unit Pro,quantity_changed,45.0,42.0,-3.0,Warehouse A,False,,
SKU-009,Mounting Bracket Small,quantity_changed,1000.0,975.0,-25.0,Warehouse B,False,,
SKU-010,Mounting Bracket Large,quantity_changed,750.0,720.0,-30.0,Warehouse B,False,,
SKU-011,LED Panel 12x12,quantity_changed,120.0,115.0,-5.0,Warehouse A,False,,
SKU-012,LED Panel 24x24,quantity_changed,90.0,85.0,-5.0,Warehouse A,False,,
SKU-013,Thermal Paste Tube,quantity_changed,2000.0,1850.0,-150.0,Warehouse C,False,,
SKU-014,Cooling Fan 80mm,quantity_changed,300.0,290.0,-10.0,Warehouse B,False,,
SKU-015,Cooling Fan 120mm,quantity_changed,250.0,245.0,-5.0,Warehouse B,False,,
SKU-016,USB Hub 4-Port,quantity_changed,180.0,165.0,-15.0,Warehouse A,False,,
SKU-017,USB Hub 7-Port,quantity_changed,95.0,88.0,-7.0,Warehouse A,False,,
SKU-018,Ethernet Cable Cat5,quantity_changed,800.0,750.0,-50.0,Warehouse C,False,,
SKU-019,Ethernet Cable Cat6,quantity_changed,600.0,580.0,-20.0,Warehouse C,False,,
SKU-020,Ethernet Cable Cat6a,quantity_changed,400.0,390.0,-10.0,Warehouse C,False,,
SKU-021,HDMI Cable 3ft,quantity_changed,450.0,425.0,-25.0,Warehouse A,False,,
SKU-022,HDMI Cable 6ft,quantity_changed,380.0,365.0,-15.0,Warehouse A,False,,
SKU-023,HDMI Cable 10ft,quantity_changed,220.0,210.0,-10.0,Warehouse A,False,,
SKU-024,DisplayPort Cable,quantity_changed,175.0,170.0,-5.0,Warehouse A,False,,
SKU-025,VGA Cable,removed,50.0,,,Warehouse B,False,,
SKU-026,DVI Cable,removed,35.0,,,Warehouse B,False,,
SKU-027,Audio Cable 3.5mm,quantity_changed,600.0,575.0,-25.0,Warehouse C,False,,
SKU-028,Audio Cable RCA,quantity_changed,400.0,385.0,-15.0,Warehouse C,False,,
SKU-029,Optical Audio Cable,quantity_changed,150.0,145.0,-5.0,Warehouse C,False,,
SKU-030,Surge Protector 6-Outlet,quantity_changed,200.0,188.0,-12.0,Warehouse A,False,,
SKU-031,Surge Protector 12-Outlet,quantity_changed,120.0,112.0,-8.0,Warehouse A,False,,
SKU-032,Extension Cord 10ft,quantity_changed,300.0,285.0,-15.0,Warehouse B,False,,
SKU-033,Extension Cord 25ft,quantity_changed,180.0,172.0,-8.0,Warehouse B,False,,
SKU-034,Power Strip,quantity_changed,250.0,240.0,-10.0,Warehouse A,False,,
SKU-035,Cable Ties 100pk,quantity_changed,1500.0,1420.0,-80.0,Warehouse C,False,,
SKU-036,Cable Ties 500pk,quantity_changed,400.0,385.0,-15.0,Warehouse C,False,,
SKU-037,Velcro Straps 50pk,quantity_changed,800.0,765.0,-35.0,Warehouse C,False,,
SKU-038,Label Maker,quantity_changed,25.0,22.0,-3.0,Warehouse A,False,,
SKU-039,Label Tape,quantity_changed,200.0,185.0,-15.0,Warehouse A,False,,
SKU-040,Screwdriver Set,quantity_changed,150.0,142.0,-8.0,Warehouse B,False,,
SKU-041,Precision Screwdriver Set,quantity_changed,100.0,95.0,-5.0,Warehouse B,False,,
SKU-042,Wire Stripper,quantity_changed,75.0,70.0,-5.0,Warehouse B,False,,
SKU-043,Crimping Tool,quantity_changed,60.0,58.0,-2.0,Warehouse B,False,,
SKU-044,Multimeter Basic,quantity_changed,40.0,35.0,-5.0,Warehouse A,False,,
SKU-045,Multimeter Professional,quantity_changed,25.0,23.0,-2.0,Warehouse A,True,Multimeter Pro,Multimeter Professional
SKU-046,Soldering Iron,quantity_changed,35.0,32.0,-3.0,Warehouse B,False,,
SKU-047,Solder Wire,quantity_changed,300.0,280.0,-20.0,Warehouse B,False,,
SKU-048,Heat Shrink Tubing,quantity_changed,500.0,475.0,-25.0,Warehouse C,False,,
SKU-049,Electrical Tape,quantity_changed,800.0,760.0,-40.0,Warehouse C,False,,
SKU-050,Anti-Static Wrist Strap,quantity_changed,200.0,190.0,-10.0,Warehouse A,False,,
SKU-051,Anti-Static Mat,quantity_changed,50.0,48.0,-2.0,Warehouse A,False,,
SKU-052,Compressed Air Can,quantity_changed,400.0,375.0,-25.0,Warehouse C,False,,
SKU-053,Isopropyl Alcohol 99%,quantity_changed,150.0,140.0,-10.0,Warehouse C,False,,
SKU-054,Microfiber Cloth 10pk,quantity_changed,300.0,285.0,-15.0,Warehouse C,False,,
SKU-055,Screen Cleaner,quantity_changed,250.0,235.0,-15.0,Warehouse C,False,,
SKU-056,Keyboard Cleaner Gel,quantity_changed,180.0,168.0,-12.0,Warehouse C,False,,
SKU-057,Monitor Stand,quantity_changed,45.0,42.0,-3.0,Warehouse A,False,,
SKU-058,Laptop Stand,quantity_changed,60.0,58.0,-2.0,Warehouse A,False,,
SKU-059,Tablet Stand,quantity_changed,80.0,75.0,-5.0,Warehouse A,False,,
SKU-060,Phone Stand,quantity_changed,120.0,115.0,-5.0,Warehouse A,False,,
SKU-061,Desk Organizer,quantity_changed,90.0,85.0,-5.0,Warehouse B,False,,
SKU-062,Cable Management Box,quantity_changed,110.0,105.0,-5.0,Warehouse B,False,,
SKU-063,Headphone Hook,quantity_changed,200.0,192.0,-8.0,Warehouse B,False,,
SKU-064,Webcam Mount,quantity_changed,75.0,72.0,-3.0,Warehouse A,False,,
SKU-065,Ring Light 10in,quantity_changed,40.0,38.0,-2.0,Warehouse A,False,,
SKU-066,Ring Light 18in,quantity_changed,25.0,22.0,-3.0,Warehouse A,False,,
SKU-067,Tripod Small,quantity_changed,55.0,52.0,-3.0,Warehouse B,False,,
SKU-068,Tripod Large,quantity_changed,35.0,33.0,-2.0,Warehouse B,False,,
SKU-069,Green Screen,quantity_changed,20.0,18.0,-2.0,Warehouse A,False,,
SKU-070,Backdrop Stand,quantity_changed,15.0,12.0,-3.0,Warehouse A,False,,
SKU-071,USB Microphone,quantity_changed,30.0,28.0,-2.0,Warehouse A,False,,
SKU-072,XLR Microphone,quantity_changed,20.0,18.0,-2.0,Warehouse A,False,,
SKU-073,Pop Filter,quantity_changed,100.0,95.0,-5.0,Warehouse B,False,,
SKU-074,Boom Arm,quantity_changed,45.0,42.0,-3.0,Warehouse B,False,,
SKU-075,Shock Mount,quantity_changed,40.0,38.0,-2.0,Warehouse B,False,,
SKU-076,Stream Deck Mini,added,,15.0,,Warehouse A,False,,
SKU-077,Stream Deck XL,added,,8.0,,Warehouse A,False,,
SKU-078,Capture Card,added,,12.0,,Warehouse A,False,,
SKU-079,USB-C Hub,added,,45.0,,Warehouse A,False,,
SKU-080,Thunderbolt Cable,added,,30.0,,Warehouse A,False,,
Loading