Initializing
Back to Projects
Year2024
DomainFullstack
AccessOpen Source
Complexity9.2 / 10
Live Link
NestJSNext.jsPostgreSQLPrismaTypeScriptTailwind CSSJWTPassportDockerPM2TraefikRechartsFramer MotionExcelJSPDFKit
FullstackProduction

MBTCMS — Municipal Board Tax Collection & Management System

Production-grade tax administration platform for Municipal Boards with Arrear/Current financial model, automated demand generation, multi-mode payments, daily reconciliation, and comprehensive audit logging.

Tenant Records0+
Monthly Transactions0+
API Endpoints0+
Database Models0
Audit Events Logged0+

Table of Contents


The Challenge

Municipal Boards in India manage thousands of commercial properties including shops, stalls, and market spaces. The tax collection process involves complex financial workflows that must maintain strict accounting integrity across fiscal year boundaries while providing accessible interfaces for both administrators and field collectors.

Prior to this system, Bajali Municipal Board relied on manual register-based tracking of tenant payments, demand generation through handwritten bills, and reconciliation through physical cash counting. This approach introduced significant inefficiencies: duplicate allotments due to lack of occupancy status tracking, inconsistent application of arrears and current dues, inability to generate comprehensive DCB (Demand-Collection-Balance) reports, and no audit trail for financial mutations.

The fundamental challenge was building a system that could handle the complete tax administration lifecycle while enforcing strict financial rules around the Arrear/Current model. Every payment must be automatically allocated to oldest arrears first, demand generation must respect fiscal year boundaries, and financial year transitions (period close) must lock all transactions while carrying forward balances correctly.

The requirement: Build a production-grade tax administration platform with automated demand generation, multi-mode payment collection (Cash/Online/Cheque), daily reconciliation, DCB reporting, RBAC with three tiers (Admin/Superuser/Tax Collector), full audit logging, and deployment-ready Docker configuration.


Architecture & Solution

MBTCMS follows a three-layer architecture designed for scalability and audit integrity, implemented as a monorepo using NPM workspaces:

Parsing system architecture diagram...

Three-Layer Model

LayerComponentsPurpose
ConfigurationMasters, Roles, Permissions, Financial YearsSetup once, reference many
OperationsAllotment, Demand Generation, Payment CollectionRecurring daily/cyclical
ControlReconciliation, Adjustments, Audit Log, ReportsOversight and accountability

Core Engine Services

The system implements five core services handling all critical financial logic:

ServiceResponsibility
Demand ServiceAutomated monthly/yearly demand calculation with intelligent arrear rollover
Payment ServiceMulti-mode collection with auto-allocation to arrears first
Adjustment ServiceRemissions with mandatory justification and Arrear/Current scope
Period-Close ServiceFinancial year transitions, transaction locking, balance rollover
Reconciliation ServiceDaily cash/online/bank verification with carry-forward logic

Tech Stack

LayerTechnologyVersion
RuntimeNode.js20 LTS
Backend FrameworkNestJS11.x
Frontend FrameworkNext.js (App Router)16.x
UI LibraryReact19.x
StylingTailwind CSS4.x
AnimationFramer Motion12.x
ChartsRecharts3.x
IconsLucide React0.563+
DatabasePostgreSQL15
ORMPrisma6.x
AuthJWT + Passport
API DocsSwagger/OpenAPIvia @nestjs/swagger
Process ManagerPM2
Reverse ProxyTraefik
Excel ExportExcelJS4.x
PDF GenerationPDFKit
Package ManagerNPM Workspaces

Key Engineering Decisions

1. Strict Arrear/Current Financial Model

Every financial transaction maintains explicit separation between arrear dues (from previous financial years) and current dues (current year). This model ensures accounting integrity across fiscal year boundaries.

typescript
// payments.service.ts - Auto-allocation logic
async create(createPaymentDto: CreatePaymentDto) {
  const payment = await tx.payment.create({
    data: {
      amount: createPaymentDto.amount,
      arrearAmount: createPaymentDto.arrearAmount || 0,
      currentAmount: createPaymentDto.currentAmount || 0,
      // ...
    },
  });

  // Update Allotment balances: arrear first, then current
  if (createPaymentDto.allotmentId) {
    const allotment = await tx.allotment.findUnique({
      where: { id: createPaymentDto.allotmentId },
    });

    let remainingAmount = Number(createPaymentDto.amount);

    // Apply to Arrears first
    if (remainingAmount > 0 && Number(allotment.arrearDemand) > 0) {
      const arrearPayment = Math.min(remainingAmount, Number(allotment.arrearDemand));
      await tx.allotment.update({
        where: { id: createPaymentDto.allotmentId },
        data: { arrearDemand: { decrement: arrearPayment } },
      });
      remainingAmount -= arrearPayment;
    }

    // Apply remaining to Current
    if (remainingAmount > 0) {
      await tx.allotment.update({
        where: { id: createPaymentDto.allotmentId },
        data: { currentDemand: { decrement: remainingAmount } },
      });
    }
  }
}

The system prevents negative balances through Math.max(0, val) clamping across all financial operations, ensuring data integrity even under edge cases.

2. Financial Year Lifecycle Management

The system implements a strict financial year state machine with three phases: OPEN, ADJUSTMENT_WINDOW (7 days after year end), and CLOSED. All financial operations are blocked when no active financial year exists.

typescript
// period-close.service.ts
async validateTransactionAllowed(financialYear: string) {
  const fy = await this.prisma.financialYear.findFirst({
    where: { year: financialYear },
  });

  if (!fy) {
    throw new BadRequestException('No active financial year found. Please configure a financial year first.');
  }

  if (fy.status === 'CLOSED') {
    throw new BadRequestException('Financial year is closed. New transactions are not allowed.');
  }
}

async closeFinancialYear(year: string) {
  return this.prisma.$transaction(async (tx) => {
    // 1. Calculate new arrear: Old Arrear + Old Current
    const allotments = await tx.allotment.findMany({
      where: { status: 'ACTIVE' },
    });

    for (const allotment of allotments) {
      const newArrear = Number(allotment.arrearDemand) + Number(allotment.currentDemand);
      await tx.allotment.update({
        where: { id: allotment.id },
        data: {
          arrearDemand: newArrear,
          currentDemand: 0,
        },
      });
    }

    // 2. Mark FY as closed
    await tx.financialYear.update({
      where: { year },
      data: { status: 'CLOSED', closedAt: new Date() },
    });
  });
}

3. Comprehensive Audit Logging

Every data mutation is captured through a global interceptor that records user context, IP address, user agent, old and new values:

typescript
// common/interceptors/audit.interceptor.ts
@Injectable()
export class AuditInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const user = request.user;

    return next.handle().pipe(
      tap(async (data) => {
        if (this.isMutatingRequest(request.method)) {
          await this.prisma.auditLog.create({
            data: {
              userId: user?.id,
              userName: user?.name,
              action: `${request.method} ${request.route.path}`,
              entity: this.extractEntityName(request.route.path),
              entityId: data?.id,
              oldValue: request.method !== 'POST' ? await this.getOldValue(request) : null,
              newValue: data,
              remark: request.body.remark,
              ipAddress: request.ip,
              userAgent: request.headers['user-agent'],
            },
          });
        }
      }),
    );
  }
}

4. Collector Auto-Resolution from JWT

Tax collector identity is automatically resolved from the JWT token context, eliminating manual collector selection and ensuring all transactions are attributable:

typescript
// payments.controller.ts
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('ADMIN', 'SUPERUSER', 'TAX_COLLECTOR')
@Post()
async create(@Body() createPaymentDto: CreatePaymentDto, @Request() req) {
  // Collector ID is automatically extracted from JWT
  createPaymentDto.collectorId = req.user.taxCollector?.id;
  return this.paymentsService.create(createPaymentDto);
}

5. Occupancy Management

Room and asset status prevents double-allotment through explicit VACANT/OCCUPIED states:

typescript
// allotments.service.ts
async create(createAllotmentDto: CreateAllotmentDto) {
  // Check room availability
  const room = await this.prisma.room.findUnique({
    where: { id: createAllotmentDto.roomId },
  });

  if (room.status !== 'VACANT') {
    throw new BadRequestException('Room is not available for allotment. Select a VACANT room.');
  }

  // Create allotment and update room status
  const allotment = await this.prisma.allotment.create({
    data: { ...createAllotmentDto },
  });

  await this.prisma.room.update({
    where: { id: createAllotmentDto.roomId },
    data: { status: 'OCCUPIED' },
  });

  return allotment;
}

Database Schema

The shared Prisma schema defines 20 core models across three functional domains:

Masters Layer

ModelPurpose
WardGeographic administrative divisions
StreetStreets within wards
MarketMarkets within streets
RoomIndividual shop/stall units in markets
BankBank master for payment tracking
TaxCollectorField collector registry
DistributionUsage type distribution
WorkAssignmentCollector area assignments

Operations Layer

ModelPurpose
TenantShop/stall tenant registry with unique TNT-XXXX codes
AllotmentShop assignment with rent and security deposit
DemandMonthly tax bills with Bill IDs
PaymentCollection records with Arrear/Current breakdown
AdjustmentRemissions and corrections
LeaseAllotmentLeased market contracts
LeasePaymentLease rental collections
LessieLeased market lessee registry
LeasedAssetMarkets, stalls, open spaces
MiscReceiptNon-tax income receipts
MiscReceiptTypeReceipt category master

Control Layer

ModelPurpose
FinancialYearFY lifecycle (OPEN/CLOSED)
DailyReconciliationCollector daily cash verification
AuditLogComprehensive mutation trail
SystemSettingConfiguration key-value store

Core Modules

Room Rent Management

The system manages shop and stall allotments through a complete lifecycle:

FeatureImplementation
Tenant EngineLifecycle tracking (ACTIVE/NOTICE/LEFT), unique TNT-XXXX codes
Allotment EngineShop/stall assignment with rent amount, security deposit
Revocation SystemContract termination with automatic VACANT status update
Tax Demand EngineAutomated monthly billing with unique Bill IDs
Payment RecoverySplit-payment mode (Cash+Online+Cheque), auto-allocated to arrears

Leased Market Management

Comprehensive handling of leased assets:

FeatureImplementation
Leased AssetsMarkets, stalls, open spaces with ward/street mapping
Lessie RegistryPersonal, Firm, Company types with Aadhar/PAN
Lease AllotmentsQuarterly billing, security deposit, order numbers
Lease RevocationSecurity deposit settlement (Refund/Forfeit/Deduct)

Miscellaneous Receipts

Non-tax income management:

FeatureImplementation
Receipt Type MastersConfigurable categories with Ledger Code
Quick ReceiptingFast form for recording income with payer identity

Daily Reconciliation

End-of-day verification workflow:

FeatureImplementation
Unified VerificationCash, Online, Bank daily by collector
Variance DetectionPhysical vs digital discrepancy calculation
Carry ForwardUnverified amounts roll to next period
Reconciliation GuardBlocks collection if overdue reports exist

Financial Engine

Demand Generation

The demand service generates monthly tax bills automatically:

typescript
// demands.service.ts - Single demand generation
async generateSingle(dto: GenerateSingleDemandDto) {
  const allotment = await this.prisma.allotment.findUnique({
    where: { id: dto.allotmentId },
  });

  // baseTax = Current month's rent from Allotment
  // arrears = Allotment's arrearDemand (previous FY balance)
  const baseTax = Number(allotment.rentAmount);
  const arrears = Number(allotment.arrearDemand);
  const billId = await this.codeGenerator.generateBillId();

  const demand = await tx.demand.create({
    data: {
      allotmentId: dto.allotmentId,
      financialYear: dto.financialYear,
      month: dto.month,
      baseTax,
      arrears,
      totalDemand: baseTax, // Only monthly rent is billed
      billId,
    },
  });

  // Update Allotment current demand
  await tx.allotment.update({
    where: { id: dto.allotmentId },
    data: { currentDemand: { increment: baseTax } },
  });

  return demand;
}

Receipt Number Generation

Automatic receipt numbering with daily reset:

typescript
// payments.service.ts
private async generateReceiptNo(): Promise<string> {
  const today = new Date();
  const datePrefix = `MBTCMS-${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;

  const lastPayment = await this.prisma.payment.findFirst({
    where: { receiptNo: { startsWith: datePrefix } },
    orderBy: { receiptNo: 'desc' },
  });

  let nextSeq = 1;
  if (lastPayment) {
    const lastSeq = parseInt(lastPayment.receiptNo.split('-')[4], 10);
    nextSeq = lastSeq + 1;
  }

  return `${datePrefix}-${String(nextSeq).padStart(4, '0')}`;
}

Security & Access Control

Three-Tier RBAC

RolePermissions
ADMINFull system access: Masters, Allotments, Demands, Payments, Users, Reports, Financial Year management
SUPERUSERAdministrative operations without user management
TAX_COLLECTORRead-only Masters; can record Payments, Lease Payments, Misc Receipts, submit Daily Reconciliations; cannot create/update/delete Masters, Allotments, Tenants

Guard Implementation

Role enforcement applied at both backend and frontend:

typescript
// common/guards/roles.guard.ts
@Injectable()
export class RolesGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    return super.canActivate(context);
  }

  handleRequest(err, user, info) {
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }
}

// Usage in controllers
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('ADMIN')
@Delete(':id')
async delete(id: string) { ... }

Security Features

  • Active FY Guard: Blocks all financial operations if no Financial Year is open
  • Financial Ledger Guards: Math.max(0, val) clamping prevents negative balances
  • Double-Click Protection: Lock guards on all transactional submit buttons
  • Occupancy Management: VACANT/OCCUPIED status prevents double-allotment

Deployment

Docker Quickstart

bash
# Clone and configure
git clone <repository-url>
cd mbtcms

# Configure environment
cp .env.example .env
# Edit .env with POSTGRES_PASSWORD and JWT_SECRET

# Start with Docker Compose
cd deployment
docker-compose up -d

Services started:

  • mbtcms-web → Frontend on port 3000
  • mbtcms-api → Backend on port 3001
  • mbtcms-postgres → PostgreSQL on port 5435

Production (Dokploy/Traefik)

bash
# Deploy script
bash deploy.sh

Configuration for Dokploy with Traefik reverse proxy and automatic HTTPS via Let's Encrypt.

PM2 (Bare Metal)

bash
npm run build --workspace=backend
npm run build --workspace=frontend
pm2 start ecosystem.config.js

Default Access

URLhttp://localhost:3000
Admin Email[email protected]
Admin Passwordadmin123

Roadmap

  • Phase 1 — Core Infrastructure (Complete) - NestJS, Next.js, Prisma, PostgreSQL
  • Phase 2 — Room Rent Module (Complete) - Tenant, Allotment, Demand, Payment
  • Phase 3 — Leased Market Module (Complete) - Assets, Lessies, Lease Allotments, Payments
  • Phase 4 — Misc Receipts (Complete) - Receipt types, quick receipting
  • Phase 5 — Daily Reconciliation (Complete) - Cash/Online/Bank verification
  • Phase 6 — RBAC Implementation (Complete) - Three-tier role system
  • Phase 7 — Audit Logging (Complete) - Global interceptor for all mutations
  • Phase 8 — Reports & Analytics (Complete) - DCB, Revenue, Visualization
  • Phase 9 — Mobile App (In Progress) - React Native companion for collectors
  • Phase 10 — Payment Gateway Integration (Planned) - Online payment collection

Conclusion

MBTCMS provides Bajali Municipal Board with a comprehensive, production-grade tax administration platform that manages the complete lifecycle of shop and stall allotments. The strict Arrear/Current financial model ensures accounting integrity across fiscal year boundaries, while automated demand generation and payment allocation reduce manual workload significantly.

The system handles over 2,000 tenant records and 5,000+ monthly transactions with a comprehensive audit trail logging every data mutation. The three-tier RBAC ensures appropriate access controls for administrators, supervisors, and field collectors, with automatic collector resolution from JWT tokens eliminating manual assignment errors.

Production deployment through Docker with Traefik reverse proxy ensures reliable, secure access via HTTPS. The modular architecture supports future extensions including mobile applications for field collectors and payment gateway integration for online collections.

Architecture Feedback

Spotted a potential optimization or antipattern? Let me know.

Submit a Technical Suggestion