Retour aux projets
B2B Wholesale Panel

Rooqn

The backend panel that powers Odigix's wholesale operations. Resellers manage their inventory, track margins, and reconcile accounts — all in real time.

Voir le site
LaravelVue.jsInertia.jsREST APIs

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:

TierMonthly VolumeDiscount
Bronze< 1,000 USDBase price
Silver1,000 - 5,000 USD-2%
Gold5,000 - 20,000 USD-4%
Platinum20,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:

  1. Each provider has a sync job that runs every 5 minutes
  2. The job pulls the full product list from the provider's API
  3. Products are matched to the internal catalog by SKU mapping
  4. Price and stock updates are applied to the product_supplier table
  5. The available_products view 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:

  1. Their balance gets debited
  2. The order gets sent to the upstream provider
  3. 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

Vous avez un projet similaire ?

Expliquez-moi où vous en êtes et ce qui vous bloque. Réponse rapide sur WhatsApp, devis gratuit.