Skip to content
BUI CHI DUNG
  • AboutSections
  • ExpertiseSections
  • ProjectsSections
  • PrinciplesSections
  • AI EngineeringSections
  • ExperienceSections
  • ContactSections
  • E-Budget case studyCase studies
  • ESG System case studyCase studies
  • AI Sales Forecasting case studyCase studies
  • Digisales case studyCase studies
  • Salesportal case studyCase studies
  • Download CVLinks
  • Send an emailLinks
  • LinkedInLinks

15 results

Back to projects

Case study

Salesportal

Enterprise Sales Portal Backend

The backend of an enterprise sales portal for managing orders, customers, pricing, discounts, approvals and notifications, with two-way ERP integration.

Excel Technologies · December 2024 – May 2025

Project scale · counted from the repository

Source files
~700
Entities
107
Controller files
65
Route decorators
501
Background queues
8

Problem and goals

Sales operations, from orders to approvals, had to run in one portal where each user sees only the data that belongs to them, while prices and order numbers stay in step with the ERP system.

  • One portal for sales

    Orders, customers, pricing, discounts, cancellations, refunds and notifications are handled in one backend.

  • Access by role

    Employees, customers and sub-accounts receive permissions through roles, with default roles created for each role type.

  • Data scoped by sales area

    A user sees only the orders, customers and items within their own sales area and customer relationships.

  • In step with the ERP system

    The ERP system remains the source of prices, discounts and order numbers; the portal sends orders out and mirrors the results back.

My contribution

Taken from my CV. The rest of this page describes the system, not who built each part.

  • Designed and implemented role-based access control across multiple business units and organization hierarchies.
  • Built authorization logic for dynamic feature access based on user roles and organizational structures.
  • Improved the maintainability of permission handling by centralizing access-control logic.

System architecture

Request path

  1. Clients

    Web and mobile clients calling the REST API with JWT.

  2. Guards

    Authentication and a permission guard on controller endpoints.

  3. Controllers

    Entry point of each domain module.

  4. Services

    Business logic for orders, customers, pricing, approvals and more.

  5. PostgreSQL

    Main store, accessed through TypeORM.

Supporting services

  • Redis

    Permission cache and the backend for Bull queues.

  • Bull queues

    Eight queues for inbound and outbound data, notifications and bulk imports.

  • ERP integration

    Outbound calls, plus inbound pushes processed by queues.

  • MongoDB

    Access logs and a log of every ERP call.

  • Push notifications

    Delivered through Firebase Cloud Messaging.

  • Excel export

    Reports generated with ExcelJS.

  • Observability

    Prometheus metrics and Loki logging.

Domain modules A modular NestJS monolith.

Simplified from the repository. It shows layers and supporting capabilities, not a deployment topology.

Engineering deep dives

Permission model, default roles and cache

What each kind of account may do, and how that is looked up quickly.

Documented design

  • A catalog of permission groups and permission masters defines everything that can be granted.
  • Roles for employees, customers and sub-accounts are built from that catalog, and default roles are created and synchronized per role type.
  • A permission guard protects controller endpoints.
  • An account's permission tree is cached in Redis, warmed at employee login and invalidated when a role's permissions change, a role is assigned or a token device is deactivated.
  • A legacy rule model and the newer permission model coexist.
From catalog to account
  1. Permission group

    A category of permissions

  2. Permission master

    One grantable permission

  3. Role

    A set of permissions

  4. Account

    Employee, customer or sub-account

Row-level data scoping by sales area

Which rows a user may touch, beyond what they may do.

Documented design

  • A user's data access is built from their customer relationships and their data-access scopes.
  • Scopes cover sales organization, division, site and sales office, and a position hierarchy applies.
  • An empty scope denies access.
  • The filter was extended from orders to the other modules.
Decision for each request
The user's customer relationships and data-access scopes

Does the user have any scope?

  • Yes

    Rows are limited to the matching scopes

  • No

    No rows are returned

    Access is denied by default

ERP integration through queues

Keeping the portal and the ERP system in step.

Documented design

  • The portal sends orders to the ERP system and maps the returned prices and discounts onto local order lines, then recomputes the totals.
  • Responses are validated, and a mismatch in order lines raises an error.
  • Every ERP call is logged to MongoDB with its status.
  • Orders that failed to send can be re-sent in chunks.
  • The ERP system also pushes data to inbound endpoints, which Bull queues process in batches.
Order round trip
  1. Order created

    In the portal

  2. Sent to the ERP system

  3. Prices and discounts mapped back

  4. Totals recomputed

Engineering trade-offs

  • Caching permissions in Redis

    Why it matters
    It avoids a database lookup on every guarded request.
    What it costs
    Freshness depends on invalidation covering every flow that changes a role.
  • Keeping scoping rules in shared helpers

    Why it matters
    One place to change how data access is decided.
    What it costs
    Shared helpers tend to grow large and touch many modules.
  • The ERP system as the source of truth for prices

    Why it matters
    Prices and discounts are computed in one place.
    What it costs
    Portal correctness depends on the ERP's responses, so they must be validated.
  • A newer permission model beside the legacy one

    Why it matters
    It avoids switching everything at once.
    What it costs
    Two models have to be maintained until the old one is retired.

Lessons learned

  1. Separate what a user may do from which rows they may see

    Permissions and data scoping answer different questions, and keeping them apart lets each change on its own.

  2. Design cache invalidation with the cache

    A cached permission tree is only as fresh as the flows that clear it, so every role-changing path needs to be covered.

  3. Verify what the source of truth returns

    When another system owns prices, validating its responses is part of the integration.

Tech stack

  • Backend

    • NestJS
    • TypeScript
  • Data

    • PostgreSQL
    • TypeORM
    • MongoDB
  • Cache and queues

    • Redis
    • Bull
  • Authentication

    • Passport-JWT
    • CASL
  • Files

    • ExcelJS
  • Notifications

    • Firebase Cloud Messaging
  • Operations

    • Docker
    • pm2
    • Prometheus
    • Loki