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

Digisales

Field Sales Management Backend

The backend of a field sales management app: jobs that move through a confirmation workflow, recurring tasks generated on a schedule, product quality feedback with deadline warnings, approval flows for sales plans, and Excel imports.

Excel Technologies

Project scale · counted from the repository, 2026-10-13

Lines of TypeScript
~93.7k
1,006 source files
Active controllers
53
55 controller files
Route handlers
504
PostgreSQL entities
122
Plus 2 MongoDB schemas
Use case files
70
Permission codes
206
22 groups
Migrations
4
No baseline migration
Automated tests
0
Only the framework's scaffold e2e file

Problem and goals

Field sales work had to be planned, assigned, confirmed and followed up in one backend: everyday tasks and their recurrence, customer feedback about product quality with deadlines, and the periodic plans that managers confirm level by level.

  • Assign and confirm jobs

    A job is created, sent to its assignee and confirmed along the sales hierarchy before work starts.

  • Recurring work without manual setup

    Routine jobs repeat on a schedule that each job's settings define.

  • Handle quality feedback to a deadline

    Each feedback ticket has handlers and a deadline per step, and the right people are warned in time.

  • Plans confirmed level by level

    Sales plans, price updates and surveys move up a ladder of confirmations.

  • Data in by spreadsheet

    Customers, surveys, plans and prices arrive through validated Excel imports.

My contribution

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

  • Implemented task management and product quality feedback processing features.
  • Developed automated recurring task generation using cron jobs and scheduled background processes.
  • Built backend workflows to improve operational task tracking and recurring process automation.

System architecture

Request path

  1. Controllers

    Per-endpoint decorators for authentication, permissions, validation and the response envelope.

  2. Use cases

    Business logic per feature, with repositories injected by interface token.

  3. Repositories

    Interfaces in the domain layer, implemented over TypeORM with a shared base for pagination and soft delete.

  4. PostgreSQL

    The main store: 122 entities, accessed through TypeORM.

Supporting services

  • MongoDB

    API call logs and discussion messages, through Mongoose.

  • Scheduler

    @nestjs/schedule cron jobs for recurring jobs and warnings.

  • Realtime

    Socket.io with a Redis adapter for notification counts and discussion rooms.

  • Identity service

    Tokens are validated by calling an external identity service.

  • Notification and file services

    Notifications and file storage are separate services reached over HTTP.

  • Email

    nodemailer with EJS templates.

  • Excel

    Imports, exports and 27 template files with ExcelJS.

Domain modules A layered NestJS application: controller, use case, repository interface, TypeORM repository.

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

Engineering deep dives

A layered backend

How a request travels, and what each layer decorates it with.

Documented design

  • A controller calls a use case, which injects a repository interface by a string token, and a TypeORM repository implements that interface on top of a shared base repository. Domain models, enums and the error catalogs live in the domain layer.
  • Cross-cutting behaviour is applied per controller with decorators: authentication, a permission guard, a validation pipe, an interceptor that wraps results in a standard envelope with the duration, and an interceptor that writes an API log record to MongoDB.
  • Authentication calls an external identity service to validate each token; the service issues no tokens itself. Permissions are codes of the form module and action, in 22 enum groups with 206 codes. A guard accepts a list as OR, and a nested list as AND.
  • A request-scoped transaction interceptor, applied on 33 controllers, opens a transaction, hands its entity manager to the repositories through the request, and commits or rolls back at the end.
  • Entities share a base with created, updated and deleted timestamps and actor columns, and delete is soft. A shared pagination helper defaults to 15 rows and caps at 100.

Design rationale

  • Request-scoped transactions let a use case change several tables atomically without passing a transaction object through every call.
  • Interfaces between use cases and repositories keep business rules free of query code.
One request
  1. Controller

    Auth, permission, validation, envelope

  2. Use case

    Business rules

  3. Repository interface

    Injected by token

  4. TypeORM repository

    Inside the request's transaction

Shape of the codeCounted from the repository.
Active controllers
53
Use case files
70
DTO files
343

The job lifecycle

Nine statuses, rules for who may move a job, and a history of what changed.

Documented design

  • A job has an assignee, a creator, a type, dates, a status, a completion percentage, a result, document paths and a role for the assignee. Jobs can have sub-jobs, with a single parent each, and can link to customers or to a business opportunity's action plan.
  • The statuses are CREATING, WAITING_STAFF_CONFIRM, WAITING_MANAGER_CONFIRM, REJECTED, IN_PROCESS, PENDING, EXPIRED, COMPLETED and CANCEL. There is no transition table: the rules are spread across the create, update, send and confirm paths.
  • Sending moves a job out of CREATING and only the creator may send it. If the assignee is someone else and the role is not implementer, the job waits for the assignee's confirmation; an implementer goes straight to IN_PROCESS. If the sender is the assignee, a salesperson goes to IN_PROCESS and anyone else waits for the manager's confirmation.
  • The confirmer is derived from the position hierarchy, the next position up. Confirming or rejecting is allowed only for that confirmer, takes a list of job ids, and leads to IN_PROCESS or REJECTED.
  • Updates depend on the current status. Only the creator edits a CREATING job; a rejected job is edited by the line manager; moving from IN_PROCESS or PENDING allows IN_PROCESS, COMPLETED, PENDING or CANCEL; COMPLETED needs a completion date; COMPLETED, PENDING and CANCEL need a result; an EXPIRED job cannot be changed.
  • Validation requires that the end date is after the start date, that referenced samples, types and customers exist, and that a sub-job is not its own parent or already under another parent. When a job is tied to an action plan, its dates are clamped into the plan's range.
  • A history row records the actor and a task (create, edit, confirm, reject) with old and new snapshots, but only for edits made after the job has been sent. Discussion threads are stored in MongoDB and broadcast to a room for the job.

Design rationale

  • Deriving the confirmer from the position hierarchy means nobody has to pick an approver by hand.
  • Snapshots of old and new values make an edit reviewable after the fact.
From creation to work
  1. CREATING

    Only the creator edits and sends

  2. Waiting for confirmation

    Assignee or manager, depending on the sender

  3. IN_PROCESS

    Percent, result and status can change

  4. COMPLETED

    Needs a completion date and a result

A confirmer's decision
Job waiting for confirmation

Confirmer's decision

  • Yes

    IN_PROCESS

    Work starts

  • No

    REJECTED

    The line manager edits it

Recurring job generation

A nightly run that creates the next occurrence of each repeating job.

Documented design

  • A job's settings define a repeat cycle (none, daily, weekly, monthly or yearly), an interval, weekdays for the weekly cycle, how many days ahead to create the next job, whether the previous one must be completed, and a reminder.
  • A cron job runs at midnight in the local business timezone. It selects jobs that have not yet been repeated, are not cancelled, repeat on a cycle, whose next start date has arrived and, when required, are completed. It works in batches of 100.
  • Each generated job copies the source with a new id, starts directly as IN_PROCESS (skipping the confirmation workflow), keeps a link to the original and gets a copy of the settings. It becomes the next template in a chain, and the source is flagged as repeated.
  • The next date adds the interval for daily, monthly and yearly cycles, with the day clamped to the length of the target month, and takes the next listed weekday for weekly ones. The job's duration is preserved.
  • After downtime, a chain catches up at one job per run, because the next start date is compared with the current time at each run.

Design rationale

  • Chaining each occurrence from the previous one means the settings live on the job itself and no separate schedule table is needed.
  • Skipping the confirmation step for generated jobs fits routine work that was already agreed once.
The nightly run
  1. Select due templates

    Not repeated, next start date reached

  2. Create the next job

    Copy, IN_PROCESS, linked to the original

  3. Compute the next date

    Cycle, interval, weekdays, days ahead

  4. Flag the source

    Marked as repeated

Repeat cyclesCycle values in the job settings.

NO-REPEATDAILYWEEKLYMONTHLYYEARLY

Overdue and due-soon warnings

Per-minute checks that warn each person once.

Documented design

  • Two cron jobs run every minute for jobs. The overdue check selects IN_PROCESS jobs whose end date plus a configured number of minutes has passed and which have not been warned yet. The setting is a single record with the minutes and the channels (email, in-app notification); with no value, a day is used.
  • The due-soon check selects IN_PROCESS jobs whose end date minus the job's own reminder amount has passed, and notifies the assignee.
  • Each run sends in batches with a pause of 60 seconds between batches, then sets a flag on the job so that it is warned once. Editing the end date resets the overdue flag.
  • Overdue warnings go to the creator and the assignee (or the creator and the confirmer when they are the same person), by email, in-app or both, as configured. A warning does not change the job's status.

Design rationale

  • A flag per warning type keeps the per-minute query simple, at the cost of one extra column per kind of warning.
A warning
  1. Every minute

    Select jobs not yet warned

  2. Batch of jobs

    Notify by the configured channels

  3. Set the flag

    Warn once per job

ChannelsValues of the overdue setting.

EMAILNOTIFICATION

Product quality feedback

A ticket workflow with configurable handlers and a deadline per step.

Documented design

  • A feedback ticket has a code of the form prefix, year and a six-digit sequence, and moves through eleven statuses: CREATING, WAITING_ACCEPTANCE, REJECT, PENDING_INVESTIGATION, UNDER_INVESTIGATION, WAITING_APPROVAL, WAITING_FIX, WAITING_FIX_CHANGE, FIXED, COMPLETED and CLOSED.
  • Sending moves a ticket to the next status in a fixed order; if the creator is also the sales manager, creation skips straight to investigation. Rejecting is allowed only from WAITING_ACCEPTANCE and closing only from COMPLETED, each by the step's handlers or a super-admin.
  • In the investigation step a primary cause is required. If it is not yet determined, the status stays and a further handler is added, and a second submission with the cause still undetermined is refused.
  • Handlers per step come from a configuration per business unit: explicit employees and positions, which are mapped to the ticket's own sales manager, assistant, supervisor and representative. From the later steps on, the handlers also depend on the primary cause. A step with no handler fails with a message naming the step.
  • Each step has a service-level deadline in hours, plus thresholds for reaching-due and overdue warnings. After every status change the ticket's warning rows are recreated with a deadline, a reaching-due time and an overdue time, and two per-minute cron jobs notify the step's handlers once per row.
  • A status history records the old and new status and the actor. Tickets also have discussion threads, an activity history and an Excel export.

Design rationale

  • Resolving handlers from configuration and from the ticket's own hierarchy lets the same workflow serve different business units without code changes.
  • Recreating the warning rows after each status change keeps the deadlines tied to the step the ticket is actually in.
The main path
  1. Create and send

  2. Accept

    Reject only from here

  3. Investigate

    Primary cause required

  4. Approve and fix

  5. Complete and close

    Close only after completion

Workflow factsCounted from the code.
Statuses
11
Steps per source type
11
Per-minute warning crons
2

Approval ladders for plans

One rank-based status scheme shared by several modules.

Documented design

  • Surveys, weekly order plans, price updates and plan documents share a ladder of six statuses with ranks from 0 to 5: draft, created or submitted, and four successive confirmation levels.
  • A shared rule decides whether a status may change: with no role it is refused; a forward move needs a role whose rank is above the current status (or the lowest, sales representative role); a return must go to a lower rank and cannot be done by someone at the status's own rank.
  • An output forecast, attached to a survey, has its own approval with a history table. Requesting approval is allowed only from draft or edit and only by the owner; approving is allowed only from the awaiting state and by someone else. Each step runs in a transaction and writes a history row.
  • A business opportunity has seven statuses, and its status change records a history row (send, confirm, reject, request adjustment) and notifies the manager, the creator or the reviewers, with an email to reviewers when it becomes CREATED.

Design rationale

  • A numeric rank makes every approval module answer the same question the same way: is this person above the current status?
The status ladderStatus values shared by the surveys, weekly plans and price updates, in rank order.

DRAFT (0)CREATED (1)Level 1 confirmed (2)Level 2 confirmed (3)Level 3 confirmed (4)Level 4 confirmed (5)

Excel imports and notifications

Validated spreadsheets in, and several channels out.

Documented design

  • There are 17 multipart Excel import endpoints, for customers, surveys, plans, prices, output forecasts and job customers, and 27 template files for download. An import history record keeps the file name, status (waiting, in process, success or fail), type and errors.
  • The price update import is synchronous. The workbook is read from memory, the required sheets and the header and data rows are validated, and any error returns an annotated workbook so the user can fix it. If it is clean, duplicates are checked, overwriting is allowed only for drafts, and the records and items are created inside the request's transaction.
  • Notifications go through a separate notification service for the in-app list and count, over HTTP. Socket.io, with a Redis adapter, announces new notifications and carries discussion messages in rooms per business opportunity, job and feedback ticket.
  • Email uses a pooled nodemailer transport and nine EJS templates (seven for feedback statuses, one for overdue jobs and one for opportunity reviews), sent directly or through a queue service.
  • Calls to other services use axios with up to three retries, which by default cover network errors and server errors on idempotent methods.
An import
  1. Upload

    Read from memory

  2. Validate

    Sheets, header and every row

  3. Errors?

    Return an annotated workbook

  4. Create records

    Inside the request's transaction

Engineering trade-offs

  • Use cases behind repository interfaces

    Why it matters
    Business rules stay separate from HTTP and from query code.
    What it costs
    Some use case files grow to 1,000 to 2,600 lines with validation, notifications, email and export in one place, and the domain layer imports DTOs from the controller layer.
  • One transaction per request

    Why it matters
    A use case can change several tables atomically without passing a transaction around.
    What it costs
    A long import holds its transaction for the whole request.
  • Flags instead of locks for scheduled work

    Why it matters
    A flag per job and per warning type keeps the cron queries simple.
    What it costs
    Duplicate protection assumes a single running process; nothing locks the rows between instances.
  • A shared rank ladder for approvals

    Why it matters
    Every module answers who may move a status the same way.
    What it costs
    The approval logic is repeated in several modules, and the output forecast adds a separate status scheme.

Lessons learned

  1. Put recurrence on the job itself

    Keeping the repeat settings with the job and chaining each occurrence from the last avoids a separate schedule and makes catching up after downtime a matter of dates.

  2. Resolve approvers from the hierarchy

    Deriving a confirmer from the position hierarchy, or from configuration per business unit, removes a manual choice from every workflow.

  3. Decide how a scheduled job stays safe

    A flag is enough for one process, but the assumption has to be written down, because adding a second instance changes it.

Known limitations

  • There are no automated tests: the only test file is the framework's scaffold e2e test, and the repository has no CI configuration.
  • Only four migrations exist and none is a baseline, so the earlier schema is not tracked in this repository; schema synchronization is switched off.
  • Scheduled generation and warnings rely on flags, so they assume a single running process.
  • A business opportunity's status change checks who may change it, not which target statuses are allowed.
  • Several integrations in the dependency list (a message queue, push notifications and a chat webhook) have only commented-out code, and the health controller has no routes.

Tech stack

  • Backend

    • NestJS 10
    • TypeScript 4.7
    • Node.js 18
  • Data

    • PostgreSQL
    • TypeORM 0.3
    • MongoDB (Mongoose 7)
  • Realtime

    • Socket.io 4
    • Redis (Socket.io adapter)
  • Scheduling

    • @nestjs/schedule
  • Files and email

    • ExcelJS
    • nodemailer
    • EJS
  • API

    • Swagger
    • class-validator
    • axios
  • Operations

    • Docker