Overview
> Realistic scenario notice: The company described below is a composite of real operational patterns > common to Syrian retail distributors. Names, figures, and results are illustrative; they are not > drawn from a named or verified client engagement.
A mid-size household goods distributor operating five retail branches across Aleppo and its surroundings had grown organically over eight years. At its peak the business managed more than 4,000 SKUs, processed roughly 600 transactions a day across all locations, and ran a small warehouse that supplied each branch on a weekly cycle.
Growth, however, had outpaced the tools the business was using. Each branch relied on a standalone spreadsheet to record sales and a WhatsApp group to request stock replenishment. The warehouse team reconciled incoming and outgoing stock by counting shelves once a week. By 2025, these gaps were costing the business in ways that were difficult to ignore.
---
The Business Problem
Inventory That Nobody Could Trust
Without real-time visibility, the warehouse team frequently over-ordered fast-moving products to avoid running out, while slow-moving SKUs accumulated without anyone noticing. Branch managers placed duplicate requests for the same stock, and headquarters had no single view of what was sitting across the five locations at any given moment.
The result was a cycle of:
- Stockouts on popular items mid-week (before the weekly replenishment run).
- Dead stock in one branch while another branch ran short of the same item.
- Write-offs at month-end when physical counts differed from the spreadsheet totals.
Reconciliation Was a Friday Ritual
Every Friday evening, the finance team collected sales sheets from each branch by WhatsApp, then manually merged them into a master Excel file. The process took three to four hours and still produced errors — wrong branch codes, missing lines, and formatting inconsistencies — that required follow-up on Saturday.
Pricing Drift
Because each branch manager updated their own spreadsheet, promotional prices set at headquarters sometimes appeared inconsistently: one branch discounted a product, another sold it at full price, a third never received the update. Customer complaints about price discrepancies were rising.
---
Solution Design
Dragonfly Soft proposed a custom web-based platform with three integrated modules:
| Module | Purpose | |--------|---------| | Central Inventory Ledger | Single source of truth for all SKUs, quantities, and locations | | Branch POS | Lightweight browser-based cashier interface for each branch | | Replenishment Engine | Reorder alerts, branch-to-warehouse transfer requests, and approval flow |
The decision to build a custom system — rather than deploy an off-the-shelf ERP — was driven by two constraints specific to the client:
- Intermittent internet connectivity at some branches required the POS to function offline and sync when the connection was restored.
- Arabic-first interface for branch cashiers, many of whom were not comfortable with English-language software.
Technology Stack
Frontend (Branch POS) React 18 + PWA (service worker for offline queue)
Backend API Node.js (NestJS) + REST
Database PostgreSQL 16 — multi-schema per tenant
Real-time sync WebSocket (Socket.io) for live stock counters
Hosting Single VPS (Aleppo-region cloud provider) + Cloudflare CDN
Mobile companion React Native (manager stock-check app)
> Note: The code snippets throughout this article are illustrative examples of the patterns used. > They are not production-ready and omit authentication, error handling, and environment configuration.
---
Implementation Walkthrough
1. Data Modelling: One Inventory, Many Views
The core insight was to maintain one inventory record per SKU at the warehouse level and derive branch quantities through movements rather than storing branch stock as a separate table. This avoided the synchronisation problem entirely.
-- Core tables (simplified)
CREATE TABLE products (
id SERIAL PRIMARY KEY,
sku TEXT UNIQUE NOT NULL,
name_ar TEXT NOT NULL,
name_en TEXT,
unit TEXT DEFAULT 'piece',
reorder_qty INTEGER DEFAULT 10
);
CREATE TABLE stock_locations (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL -- 'warehouse', 'branch_a', 'branch_b', ...
);
CREATE TABLE stock_movements (
id SERIAL PRIMARY KEY,
product_id INTEGER REFERENCES products(id),
from_loc_id INTEGER REFERENCES stock_locations(id),
to_loc_id INTEGER REFERENCES stock_locations(id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
movement_type TEXT NOT NULL, -- 'sale', 'transfer', 'adjustment', 'return'
created_at TIMESTAMPTZ DEFAULT now(),
created_by INTEGER -- staff user id
);
-- A view that materialises current stock per location
CREATE VIEW stock_balances AS
SELECT
product_id,
to_loc_id AS location_id,
SUM(quantity) AS qty_in
FROM stock_movements
GROUP BY product_id, to_loc_id
-- (a full implementation would subtract outgoing movements too)
;
Using an event-sourced movement ledger meant every stock change had a full audit trail, and generating a historic stock report for any date was simply a matter of replaying movements up to that point.
2. Offline-First POS
The branch POS was built as a Progressive Web App using a service worker to cache the application shell and queue transactions locally using IndexedDB when the network was unavailable.
// service-worker.js (illustrative)
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/transactions')) {
event.respondWith(
fetch(event.request).catch(() => {
// Network unavailable — queue locally
return cacheTransaction(event.request.clone()).then(() =>
new Response(JSON.stringify({ queued: true }), {
headers: { 'Content-Type': 'application/json' },
})
);
})
);
}
});
async function cacheTransaction(request) {
const db = await openIndexedDB('pos-queue');
const body = await request.json();
const tx = db.transaction('pending', 'readwrite');
tx.objectStore('pending').add({ ...body, timestamp: Date.now() });
return tx.complete;
}
When connectivity returned, a background sync job replayed the queue against the server in order, ensuring the central ledger remained consistent. Cashiers saw a small status indicator (green / amber / red) showing whether they were connected, but could continue working without interruption during outages.
3. Centralised Pricing with Branch Overrides
A pricing_rules table stored the base price and any active promotions by date range. The POS fetched the effective price at transaction time from the API, eliminating manual price-sheet distribution.
// pricing.service.ts (NestJS — illustrative)
async getEffectivePrice(productId: number, branchId: number, date: Date): Promise<number> {
const promotion = await this.db.query(
`SELECT promo_price FROM pricing_rules
WHERE product_id = $1
AND (branch_id = $2 OR branch_id IS NULL) -- branch-specific or global
AND valid_from <= $3 AND valid_to >= $3
ORDER BY branch_id NULLS LAST -- branch-specific overrides global
LIMIT 1`,
[productId, branchId, date]
);
if (promotion.rows.length) return Number(promotion.rows[0].promo_price);
const base = await this.db.query(
'SELECT base_price FROM products WHERE id = $1',
[productId]
);
return Number(base.rows[0].base_price);
}
Branch managers could apply local short-term discounts pending headquarters approval — an approval flow handled through a simple notification in the manager app.
4. Replenishment Alerts
The replenishment engine ran as a scheduled job every two hours. For each product at each branch, it compared current stock against the configured reorder_qty threshold and created a transfer request when stock fell below that level.
// replenishment.job.ts (illustrative)
@Cron('0 */2 * * *')
async checkLowStock() {
const lowItems = await this.db.query(`
SELECT p.id, p.name_ar, p.reorder_qty, sb.location_id,
COALESCE(SUM(CASE WHEN sm.to_loc_id = sb.location_id THEN sm.quantity ELSE 0 END)
- SUM(CASE WHEN sm.from_loc_id = sb.location_id THEN sm.quantity ELSE 0 END), 0)
AS current_qty
FROM products p
JOIN stock_locations sl ON sl.name != 'warehouse'
LEFT JOIN stock_movements sm ON sm.product_id = p.id
-- simplified — real query materialises balances first
GROUP BY p.id, p.reorder_qty, sb.location_id
HAVING current_qty < p.reorder_qty
`);
for (const item of lowItems.rows) {
await this.transferRequestService.create({
productId: item.id,
toBranchId: item.location_id,
suggestedQty: item.reorder_qty * 2, // 2× buffer
status: 'pending',
});
}
}
The warehouse supervisor received a consolidated pending-transfers dashboard each morning. With one approval click, the system generated a picking list and updated the ledger once the goods were confirmed as dispatched.
5. Reporting Dashboard
Headquarters staff accessed an Arabic-language management dashboard showing:
- Live stock levels per branch (updated via WebSocket push).
- Daily sales totals per branch and per product category.
- Transfer pipeline: pending, in-transit, delivered.
- Price compliance: flag when a transaction price deviated from the system-recorded price.
The dashboard was built with React and a lightweight charting library. Data was served from pre-aggregated daily summary tables refreshed by a nightly PostgreSQL job, keeping dashboard query times fast even on the constrained VPS.
---
Challenges and How They Were Addressed
Challenge 1: Cashier Buy-In
Branch cashiers were accustomed to handwritten receipts. The POS interface was designed with oversized Arabic touch targets, a large barcode-scan button, and a minimum number of screens between opening a transaction and printing a receipt. A one-day training session per branch was sufficient for adoption.
Challenge 2: Legacy Product Data
The business had accumulated product records across five different spreadsheets with inconsistent naming and no standard SKU scheme. The team built a one-time ETL script to merge and deduplicate the data, assigning a canonical SKU to each product and printing updated barcode labels for the warehouse shelves.
Challenge 3: Connectivity at Remote Branches
One branch consistently lost connectivity for two to three hours in the afternoon. The offline queue approach (described above) handled most of this, but the team also configured the service worker to pre-fetch the product catalogue on a nightly schedule so cashiers always had up-to-date product data even during outages.
---
Outcomes
After three months of operation across all branches, the business observed the following:
| Area | Before | After | |------|--------|-------| | Friday reconciliation | 3–4 hours manual | Automated nightly report | | Stockout incidents | Frequent mid-week | Rare (replenishment alerts active) | | Pricing discrepancies | Multiple per week | Near-zero | | Duplicate transfer requests | Common | Eliminated (single request per product-branch pair) | | Inventory write-offs | Regular monthly occurrence | Reduced significantly |
> Important: The figures above are illustrative estimates for this scenario. Real-world outcomes > depend on operational discipline, data quality, and connectivity conditions at each site.
---
Key Design Principles Applied
- Offline-first over optimistic connectivity — Syrian business environments cannot rely on consistent uptime; the system must degrade gracefully.
- Arabic-native UI — Localisation is not an afterthought; every field label, alert, and report is authored in Arabic from the start.
- Event ledger over mutable stock counts — Movements are immutable facts; balances are derived. This yields auditability without added complexity.
- Incremental rollout — The warehouse and one branch went live first, allowing the team to identify data quality issues before scaling to all five locations.
- Minimal server footprint — A single well-tuned VPS with PostgreSQL is sufficient for a few hundred concurrent users; premature cloud scaling adds cost without benefit.
---
Conclusion
A unified POS and inventory platform does not need to be a large, expensive ERP to deliver meaningful operational improvements for a Syrian multi-branch retailer. By focusing on the specific pain points — reconciliation, stockouts, and pricing drift — and designing for the realities of local connectivity and language, a custom-built system can quickly become the operational backbone of the business.
Dragonfly Soft's approach combines proven open-source technologies, an Arabic-first design philosophy, and an incremental delivery model that lets clients see value early rather than waiting for a monolithic go-live.
---
Ready to unify your branches? Talk to us about building a custom inventory and POS platform tailored to your business.