Rooqn
The backend panel that powers Odigix's wholesale operations. Resellers manage their inventory, track margins, and reconcile accounts — all in real time.
What Rooqn does
Rooqn is the B2B side of the digital goods business. While Odigix handles consumer sales, Rooqn is where resellers and bulk buyers manage their accounts — place orders, check pricing tiers, view transaction history, and reconcile their balances.
The typical user is a small business owner who buys gift cards in bulk from Odigix and resells them through their own channels (WhatsApp groups, physical shops, social media). They need transparent pricing, fast ordering, and clear financial records.
The architecture
Built with Laravel + Inertia.js + Vue.js. I chose this stack because:
- The Odigix backend is Laravel too, so both products stay in one ecosystem and one set of deploy and queue tooling
- SPA-like navigation once resellers are inside — no full page reloads when browsing products or viewing order history
- Inertia keeps the mental model simple: you write a Laravel controller, return an Inertia render, and Vue picks it up. No separate API layer to maintain for the dashboard
Dynamic tier pricing
Resellers see different prices based on their volume tier. The tiers are:
| Tier | Monthly Volume | Discount |
|---|---|---|
| Bronze | < 1,000 USD | Base price |
| Silver | 1,000 - 5,000 USD | -2% |
| Gold | 5,000 - 20,000 USD | -4% |
| Platinum | 20,000+ USD | -7% |
Tier recalculation runs monthly via a scheduled job that looks at the trailing 30-day volume. But the pricing displayed on the product page is calculated in real-time from the cached tier data — if someone places a large order that bumps them to the next tier, the updated pricing appears on their next page load.
The pricing engine sits behind a Redis cache with a 60-second TTL per user. During peak hours, this cache serves 95%+ of pricing requests without hitting the database.
Real-time ledger
Every financial transaction posts to a double-entry ledger. This isn't just a transactions table — it's a proper double-entry system where every debit has a corresponding credit:
Order placed: Debit reseller.balance, Credit revenue.pending
Payment received: Debit bank.account, Credit reseller.balance
Refund issued: Debit revenue.pending, Credit reseller.balance
Resellers can see:
- Current balance (real-time)
- Pending transactions (orders placed but not yet fulfilled)
- Historical reconcilement reports (daily, weekly, monthly summaries)
- Export to CSV for their own accounting
The ledger was non-negotiable from day one. The previous setup tracked balances with a single balance column on the user table, and discrepancies kept appearing with no audit trail. Double-entry makes every movement traceable.
Provider API aggregation
Rooqn pulls pricing and stock from multiple upstream providers and presents a unified catalog. If Provider A is out of stock on a 100 EUR iTunes card, Provider B's listing shows up automatically.
The aggregation logic:
- Each provider has a
syncjob that runs every 5 minutes - The job pulls the full product list from the provider's API
- Products are matched to the internal catalog by SKU mapping
- Price and stock updates are applied to the
product_suppliertable - The
available_productsview joins the catalog with supplier data to show only products that are in stock at at least one provider
This means the catalog is always current, and resellers never see products they can't actually buy.
The reconciliation problem
This was the hardest part of the entire project. When a reseller places an order, three things need to happen in sequence:
- Their balance gets debited
- The order gets sent to the upstream provider
- The provider processes (or rejects) the order
If step 2 fails but step 1 already happened, you have a reseller who paid for nothing. If step 3 rejects but step 2 succeeded, you have an order that was fulfilled but not paid for.
I built a transactional outbox pattern:
1. Create order in 'pending' state
2. Debit reseller balance (inside the same DB transaction)
3. Insert outbox event
4. Commit transaction
-- Background worker:
5. Read outbox event
6. Call provider API
7. If success: update order to 'fulfilled', insert credit ledger entry
8. If failure: update order to 'failed', insert refund ledger entry
9. Mark outbox event as processed
The outbox ensures that even if the worker crashes between steps 6 and 7, the event will be retried on the next worker cycle. No money is lost or double-counted.
The worker polls the provider every 30 seconds until it gets a definitive response (fulfilled or rejected). Some providers take up to 5 minutes to process, so a single order might generate 10 poll attempts. I added exponential backoff after the 3rd attempt to avoid hammering slow providers.
Dashboard features
Real-time metrics: The dashboard shows today's volume, pending orders, and balance — all updated via polling every 10 seconds. I considered WebSockets but polling was simpler and the data freshness requirement isn't critical.
Order history with filters: Filter by date range, status, product category, and provider. Export to CSV. The query uses a composite index on (reseller_id, created_at, status) which keeps it fast even at 100k+ orders.
Margin calculator: Resellers can input their selling price and see their margin after the wholesale cost. This is a frontend-only feature but it's one of the most-used tools according to analytics.
Results
- Handles 500+ daily transactions across resellers
- Average reconciliation time dropped from 2 days to real-time
- Zero financial discrepancies since launch
- Used by 120+ active resellers
- 99.7% uptime on the API
Building something similar?
Tell me where you are and what's blocking you. I'll give you an honest read on how I'd approach it.