diff --git a/.github/workflow/ci-cd.yaml b/.github/workflow/ci-cd.yaml
new file mode 100644
index 0000000..5d76072
--- /dev/null
+++ b/.github/workflow/ci-cd.yaml
@@ -0,0 +1,31 @@
+name: deploy to production
+on:
+ push:
+ branches:
+ - main
+
+
+jobs:
+ build:
+ name: package application
+ runs-on: ubuntu-latest
+ needs: {other jobs}
+ if: github.ref == 'refs/heads/main'
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: '20'
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Run tests
+ run: pnpm test
+
+
+
diff --git a/.circleci/config.yml b/musango-app/.circleci/config.yml
similarity index 100%
rename from .circleci/config.yml
rename to musango-app/.circleci/config.yml
diff --git a/musango-app/.gitignore b/musango-app/.gitignore
new file mode 100644
index 0000000..f02d1c1
--- /dev/null
+++ b/musango-app/.gitignore
@@ -0,0 +1,4 @@
+.qodo
+.env
+node_modules/
+coverage/
diff --git a/musango-app/Capture.PNG b/musango-app/Capture.PNG
new file mode 100644
index 0000000..468029c
Binary files /dev/null and b/musango-app/Capture.PNG differ
diff --git a/musango-app/Dockerfile b/musango-app/Dockerfile
new file mode 100644
index 0000000..412d7bb
--- /dev/null
+++ b/musango-app/Dockerfile
@@ -0,0 +1,18 @@
+# Use official Node.js image
+FROM node:16
+
+# Set working directory
+WORKDIR /app
+
+# Copy package.json and install dependencies
+COPY package*.json ./
+RUN npm install
+
+# Copy the rest of the application
+COPY . .
+
+# Expose port 8080
+EXPOSE 8080
+
+# Start the application
+CMD ["node", "app.js"]
\ No newline at end of file
diff --git a/musango-app/Jenkinsfile b/musango-app/Jenkinsfile
new file mode 100644
index 0000000..19a04d8
--- /dev/null
+++ b/musango-app/Jenkinsfile
@@ -0,0 +1,69 @@
+pipeline {
+ agent any
+
+ environment {
+ MONGO_URI = "mongodb://localhost:27017/musango-express"
+ DOCKER_IMAGE = "hilltopconsultancy/musango"
+ DOCKER_CREDENTIALS_ID = "dockerhub-creds" // Jenkins credentials ID
+ }
+
+ stages {
+ stage('Clone Repo') {
+ steps {
+ git url: 'https://github.com/HILL-TOPCONSULTANCY/musango-app.git'
+ }
+ }
+
+ stage('Install Dependencies') {
+ steps {
+ sh 'npm install'
+ }
+ }
+
+ stage('Run Tests') {
+ steps {
+ // Ensure MongoDB is running locally (assumes it's installed on Jenkins host or as a service)
+ sh '''
+ echo "Waiting for MongoDB to be ready..."
+ until nc -z localhost 27017; do sleep 2; done
+ '''
+ sh 'npm test'
+ }
+ }
+
+ stage('Run App Locally') {
+ steps {
+ sh '''
+ nohup node app.js &
+ sleep 10
+ curl --fail http://localhost:8080/health
+ '''
+ }
+ }
+
+ stage('Build Docker Image') {
+ steps {
+ sh 'docker build -t $DOCKER_IMAGE .'
+ }
+ }
+
+ stage('Push to DockerHub') {
+ steps {
+ withCredentials([usernamePassword(credentialsId: "${DOCKER_CREDENTIALS_ID}", passwordVariable: 'DOCKER_PASSWORD', usernameVariable: 'DOCKER_USERNAME')]) {
+ sh '''
+ echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
+ docker push $DOCKER_IMAGE
+ '''
+ }
+ }
+ }
+ }
+
+ post {
+ always {
+ echo 'Cleaning up...'
+ sh 'pkill -f "node app.js" || true'
+ sh 'docker logout || true'
+ }
+ }
+}
diff --git a/musango-app/README.md b/musango-app/README.md
new file mode 100644
index 0000000..41d8001
--- /dev/null
+++ b/musango-app/README.md
@@ -0,0 +1,354 @@
+# Musango Express: Enterprise Ticket Management Platform
+
+
+
+
+
+
+
+
+
+## Overview
+
+Musango Express is a comprehensive enterprise-grade ticket management platform designed for modern transportation systems. Built with scalability and reliability in mind, this platform handles ticket booking, management, and customer communications with robust backend services and an intuitive user interface.
+
+
+
+## ποΈ Architecture Overview
+
+```mermaid
+graph TB
+ %% External Services
+ subgraph "External Services"
+ USER[End User]
+ SMTP[SMTP Service]
+ MongoDBExt[MongoDB Atlas
Optional Cloud DB]
+ end
+
+ %% Load Balancer Layer
+ subgraph "Load Balancer Layer"
+ LB[Application Load Balancer]
+ end
+
+ %% Application Layer
+ subgraph "Kubernetes Cluster - Application Layer"
+ subgraph "Musango Namespace"
+ subgraph "Musango Deployment"
+ POD1[Musango Pod 1
Node.js + Express]
+ POD2[Musango Pod 2
Node.js + Express]
+ POD3[Musango Pod 3
Node.js + Express]
+ end
+
+ subgraph "Musango Service"
+ SVC[ClusterIP Service
Port 8080]
+ end
+
+ subgraph "PDF Generation"
+ PUPPETEER[Puppeteer
PDF Generation Service]
+ end
+
+ subgraph "Email Service"
+ NODEMAILER[Nodemailer
Email Processing]
+ end
+ end
+ end
+
+ %% Data Layer
+ subgraph "Kubernetes Cluster - Data Layer"
+ subgraph "Database Namespace"
+ subgraph "MongoDB Deployment"
+ MONGOPOD[MongoDB Pod
mongo:6]
+ end
+
+ subgraph "MongoDB Service"
+ MONGOSVC[ClusterIP Service
Port 27017]
+ end
+
+ subgraph "Persistent Storage"
+ PVC[(Persistent Volume
Ticket Data)]
+ end
+ end
+ end
+
+ %% Monitoring Layer
+ subgraph "Monitoring Layer"
+ subgraph "Monitoring Namespace"
+ PROM[Prometheus
Metrics Collection]
+ GRAFANA[Grafana
Dashboard Visualization]
+ LOKI[Loki
Log Aggregation]
+ end
+ end
+
+ %% Internal Connections
+ POD1 --> SVC
+ POD2 --> SVC
+ POD3 --> SVC
+ SVC --> PUPPETEER
+ SVC --> NODEMAILER
+ SVC --> MONGOSVC
+ MONGOSVC --> MONGOPOD
+ MONGOPOD --> PVC
+
+ %% External Connections
+ USER --> LB
+ LB --> SVC
+ SVC --> SMTP
+ SVC -.-> MongoDBExt
+
+ %% Monitoring Connections
+ POD1 -.-> PROM
+ POD2 -.-> PROM
+ POD3 -.-> PROM
+ MONGOPOD -.-> PROM
+ PROM --> GRAFANA
+ POD1 -.-> LOKI
+ POD2 -.-> LOKI
+ POD3 -.-> LOKI
+
+ %% Styling
+ classDef external fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
+ classDef loadbalancer fill:#fff3e0,stroke:#ef6c00,stroke-width:2px;
+ classDef app fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
+ classDef data fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px;
+ classDef monitoring fill:#fff9c4,stroke:#f57f17,stroke-width:2px;
+ classDef service fill:#bbdefb,stroke:#1565c0,stroke-width:2px;
+ classDef storage fill:#d7ccc8,stroke:#4e342e,stroke-width:2px;
+
+ class USER,SMTP,MongoDBExt external;
+ class LB loadbalancer;
+ class POD1,POD2,POD3,PUPPETEER,NODEMAILER app;
+ class MONGOPOD data;
+ class PROM,GRAFANA,LOKI monitoring;
+ class SVC,MONGOSVC service;
+ class PVC storage;
+```
+
+## Features
+
+### π« Core Ticket Management
+- **Online Booking System**: Seamless ticket reservation with real-time availability
+- **PDF Ticket Generation**: Automated ticket generation with Puppeteer
+- **Email Confirmations**: Automated email notifications with Nodemailer
+- **Booking Management**: Full CRUD operations for ticket management
+- **Customer Portal**: Self-service booking modifications and cancellations
+
+### ποΈ Enterprise Architecture
+- **Microservices Ready**: Containerized architecture with Docker
+- **Database Persistence**: MongoDB with optimized queries and indexing
+- **Kubernetes Orchestration**: Production-ready deployment manifests
+- **CI/CD Pipeline**: Automated testing and deployment with CircleCI
+- **Multi-environment Support**: Development, staging, and production configurations
+
+### π§ Technical Excellence
+- **EJS Templating**: Server-side rendering with dynamic content
+- **RESTful APIs**: Clean API design for integration and extensibility
+- **Chromium Automation**: PDF generation with headless browser automation
+- **Email Integration**: SMTP integration for customer communications
+- **Health Monitoring**: Comprehensive monitoring and logging
+
+## π οΈ Tech Stack
+
+### Backend
+- **Node.js** with Express.js framework
+- **MongoDB** with Mongoose ODM
+- **EJS** for server-side templating
+- **Puppeteer** for PDF generation
+- **Nodemailer** for email services
+
+### Frontend
+- **HTML5** with semantic markup
+- **CSS3** with responsive design
+- **JavaScript** with modern ES6+ features
+- **Bootstrap** for UI components (if used)
+
+### Infrastructure
+- **Docker** for containerization
+- **Kubernetes** for orchestration
+- **CircleCI** for continuous integration
+- **AWS EC2/EKS** for cloud deployment
+- **MongoDB Atlas** (optional) for managed database
+
+## π Quick Start
+
+### Prerequisites
+
+```bash
+# Install Node.js and npm
+curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
+sudo apt-get install -y nodejs
+
+# Install Docker
+sudo apt-get update
+sudo apt-get install docker.io -y
+sudo systemctl start docker
+sudo systemctl enable docker
+
+# Install Kubernetes tools (optional)
+sudo apt-get install -y kubectl
+```
+
+### Local Development
+
+```bash
+# Clone the repository
+git clone https://github.com/CHAFAH/musango-app.git
+cd musango-app
+
+# Install dependencies
+npm install
+
+# Set up environment variables
+cp .env.example .env
+# Edit .env with your configuration
+
+# Start MongoDB with Docker
+docker run -d --name mongodb -p 27017:27017 \
+ -e MONGO_INITDB_DATABASE=musango-express \
+ mongo:6
+
+# Test database connection
+node test-db.js
+
+# Start the application
+npm run dev
+```
+
+### Docker Deployment
+
+```bash
+# Build the Docker image
+docker build -t musango-express:latest .
+
+# Run MongoDB
+docker run -d --name mongodb -p 27017:27017 \
+ -e MONGO_INITDB_DATABASE=musango-express \
+ mongo:6
+
+# Run the application
+docker run -d -p 8080:8080 \
+ --name musango-app \
+ --link mongodb:mongodb \
+ -e MONGO_URI=mongodb://mongodb:27017/musango-express \
+ -e PORT=8080 \
+ musango-express:latest
+```
+
+## βΈοΈ Kubernetes Deployment
+
+### Prerequisites
+- Kubernetes cluster (EKS, GKE, AKS, or Minikube)
+- kubectl configured for your cluster
+- Docker registry access
+
+### Deployment Steps
+
+```bash
+# Apply MongoDB deployment
+kubectl apply -f kubernetes/mongo-deployment.yaml
+
+# Apply Musango Express deployment
+kubectl apply -f kubernetes/musango-deployment.yaml
+
+# Check deployment status
+kubectl get pods,svc,deploy
+
+# Access the application
+kubectl port-forward svc/musango-service 8080:8080
+```
+
+## π§ Configuration
+
+### Environment Variables
+
+| Variable | Description | Default | Required |
+|----------|-------------|---------|----------|
+| `PORT` | Application port | `8080` | No |
+| `MONGO_URI` | MongoDB connection string | `mongodb://localhost:27017` | Yes |
+| `DB_NAME` | Database name | `musango-express` | No |
+| `EMAIL_HOST` | SMTP host for email | - | Yes |
+| `EMAIL_PORT` | SMTP port | `587` | No |
+| `EMAIL_USER` | SMTP username | - | Yes |
+| `EMAIL_PASS` | SMTP password | - | Yes |
+| `NODE_ENV` | Environment mode | `development` | No |
+
+
+
+
+
+
+### CI/CD Pipeline (CircleCI)
+
+```yaml
+# .circleci/config.yml
+version: 2.1
+jobs:
+ build:
+ docker:
+ - image: circleci/node:18
+ steps:
+ - checkout
+ - run: npm install
+ - run: npm test
+ - run: npm run build
+
+ deploy:
+ docker:
+ - image: circleci/node:18
+ steps:
+ - checkout
+ - setup_remote_docker
+ - run: docker build -t musango-express:${CIRCLE_SHA1} .
+ - run: docker push musango-express:${CIRCLE_SHA1}
+```
+
+## π Security Features
+
+- **Helmet.js**: Security headers protection
+- **CORS**: Configured cross-origin resource sharing
+- **Input Validation**: Request data sanitization
+- **Environment Configuration**: Secure credential management
+- **Docker Security**: Non-root user execution
+- **Kubernetes Security**: Pod security contexts
+
+## π Performance Optimization
+
+- **Database Indexing**: Optimized MongoDB queries
+- **Connection Pooling**: Efficient database connections
+- **Caching Ready**: Redis integration prepared
+- **Compression**: Response compression middleware
+- **Static File Serving**: Optimized asset delivery
+
+
+## π€ Contributing
+
+We welcome contributions to enhance Musango Express:
+
+1. Fork the repository
+2. Create a feature branch (`git checkout -b feature/amazing-feature`)
+3. Commit your changes (`git commit -m 'Add amazing feature'`)
+4. Push to the branch (`git push origin feature/amazing-feature`)
+5. Open a Pull Request
+
+## π License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
+
+## π Contact
+
+**Musango Express Team** - [support@musangoexpress.com](mailto:support@musangoexpress.com)
+
+**Sani Chafah** - [prsan@nebulancesystems.com](mailto:prsan@nebulancesystems.com)
+
+[](https://www.linkedin.com/in/sani-chafah/)
+[](https://github.com/CHAFAH)
+[](https://sani-chafah.com)
+
+**Project Link:** [https://github.com/CHAFAH/musango-app](https://github.com/CHAFAH/musango-app)
+
+---
+
+**β Star this repo if you found it useful!**
+
+---
+
+*Musango Express demonstrates enterprise-grade ticket management with modern DevOps practices and cloud-native deployment patterns.*
diff --git a/musango-app/app.js b/musango-app/app.js
new file mode 100644
index 0000000..f06ded9
--- /dev/null
+++ b/musango-app/app.js
@@ -0,0 +1,51 @@
+// app.js
+const express = require('express');
+const mongoose = require('mongoose');
+const path = require('path');
+const dotenv = require('dotenv');
+const morgan = require('morgan');
+
+dotenv.config();
+const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/musango-express';
+const PORT = process.env.PORT || 8080;
+
+function createServer() {
+ const app = express();
+
+ // Middleware
+ app.use(express.urlencoded({ extended: true }));
+ app.use(express.json());
+ app.use(express.static(path.join(__dirname, 'public')));
+ app.use(morgan('dev'));
+
+ // View engine
+ app.set('view engine', 'ejs');
+ app.set('views', path.join(__dirname, 'views'));
+
+ // Routes
+ app.use('/', require('./routes/index'));
+ app.use('/', require('./routes/booking'));
+
+ // Health check
+ app.get('/health', (req, res) => {
+ res.status(200).json({ status: 'ok', env: process.env.NODE_ENV || 'dev' });
+ });
+
+ return app;
+}
+
+if (require.main === module) {
+ mongoose.set('strictQuery', false);
+ mongoose.connect(MONGO_URI).then(() => {
+ console.log('Connected to MongoDB');
+ const app = createServer();
+ app.listen(PORT, () => {
+ console.log(`π Musango App is running at http://localhost:${PORT}`);
+ });
+ }).catch(err => {
+ console.error('MongoDB connection error:', err);
+ process.exit(1);
+ });
+}
+
+module.exports = { createServer };
diff --git a/musango-app/coverage/base.css b/musango-app/coverage/base.css
new file mode 100644
index 0000000..f418035
--- /dev/null
+++ b/musango-app/coverage/base.css
@@ -0,0 +1,224 @@
+body, html {
+ margin:0; padding: 0;
+ height: 100%;
+}
+body {
+ font-family: Helvetica Neue, Helvetica, Arial;
+ font-size: 14px;
+ color:#333;
+}
+.small { font-size: 12px; }
+*, *:after, *:before {
+ -webkit-box-sizing:border-box;
+ -moz-box-sizing:border-box;
+ box-sizing:border-box;
+ }
+h1 { font-size: 20px; margin: 0;}
+h2 { font-size: 14px; }
+pre {
+ font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
+ margin: 0;
+ padding: 0;
+ -moz-tab-size: 2;
+ -o-tab-size: 2;
+ tab-size: 2;
+}
+a { color:#0074D9; text-decoration:none; }
+a:hover { text-decoration:underline; }
+.strong { font-weight: bold; }
+.space-top1 { padding: 10px 0 0 0; }
+.pad2y { padding: 20px 0; }
+.pad1y { padding: 10px 0; }
+.pad2x { padding: 0 20px; }
+.pad2 { padding: 20px; }
+.pad1 { padding: 10px; }
+.space-left2 { padding-left:55px; }
+.space-right2 { padding-right:20px; }
+.center { text-align:center; }
+.clearfix { display:block; }
+.clearfix:after {
+ content:'';
+ display:block;
+ height:0;
+ clear:both;
+ visibility:hidden;
+ }
+.fl { float: left; }
+@media only screen and (max-width:640px) {
+ .col3 { width:100%; max-width:100%; }
+ .hide-mobile { display:none!important; }
+}
+
+.quiet {
+ color: #7f7f7f;
+ color: rgba(0,0,0,0.5);
+}
+.quiet a { opacity: 0.7; }
+
+.fraction {
+ font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
+ font-size: 10px;
+ color: #555;
+ background: #E8E8E8;
+ padding: 4px 5px;
+ border-radius: 3px;
+ vertical-align: middle;
+}
+
+div.path a:link, div.path a:visited { color: #333; }
+table.coverage {
+ border-collapse: collapse;
+ margin: 10px 0 0 0;
+ padding: 0;
+}
+
+table.coverage td {
+ margin: 0;
+ padding: 0;
+ vertical-align: top;
+}
+table.coverage td.line-count {
+ text-align: right;
+ padding: 0 5px 0 20px;
+}
+table.coverage td.line-coverage {
+ text-align: right;
+ padding-right: 10px;
+ min-width:20px;
+}
+
+table.coverage td span.cline-any {
+ display: inline-block;
+ padding: 0 5px;
+ width: 100%;
+}
+.missing-if-branch {
+ display: inline-block;
+ margin-right: 5px;
+ border-radius: 3px;
+ position: relative;
+ padding: 0 4px;
+ background: #333;
+ color: yellow;
+}
+
+.skip-if-branch {
+ display: none;
+ margin-right: 10px;
+ position: relative;
+ padding: 0 4px;
+ background: #ccc;
+ color: white;
+}
+.missing-if-branch .typ, .skip-if-branch .typ {
+ color: inherit !important;
+}
+.coverage-summary {
+ border-collapse: collapse;
+ width: 100%;
+}
+.coverage-summary tr { border-bottom: 1px solid #bbb; }
+.keyline-all { border: 1px solid #ddd; }
+.coverage-summary td, .coverage-summary th { padding: 10px; }
+.coverage-summary tbody { border: 1px solid #bbb; }
+.coverage-summary td { border-right: 1px solid #bbb; }
+.coverage-summary td:last-child { border-right: none; }
+.coverage-summary th {
+ text-align: left;
+ font-weight: normal;
+ white-space: nowrap;
+}
+.coverage-summary th.file { border-right: none !important; }
+.coverage-summary th.pct { }
+.coverage-summary th.pic,
+.coverage-summary th.abs,
+.coverage-summary td.pct,
+.coverage-summary td.abs { text-align: right; }
+.coverage-summary td.file { white-space: nowrap; }
+.coverage-summary td.pic { min-width: 120px !important; }
+.coverage-summary tfoot td { }
+
+.coverage-summary .sorter {
+ height: 10px;
+ width: 7px;
+ display: inline-block;
+ margin-left: 0.5em;
+ background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
+}
+.coverage-summary .sorted .sorter {
+ background-position: 0 -20px;
+}
+.coverage-summary .sorted-desc .sorter {
+ background-position: 0 -10px;
+}
+.status-line { height: 10px; }
+/* yellow */
+.cbranch-no { background: yellow !important; color: #111; }
+/* dark red */
+.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
+.low .chart { border:1px solid #C21F39 }
+.highlighted,
+.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
+ background: #C21F39 !important;
+}
+/* medium red */
+.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
+/* light red */
+.low, .cline-no { background:#FCE1E5 }
+/* light green */
+.high, .cline-yes { background:rgb(230,245,208) }
+/* medium green */
+.cstat-yes { background:rgb(161,215,106) }
+/* dark green */
+.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
+.high .chart { border:1px solid rgb(77,146,33) }
+/* dark yellow (gold) */
+.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
+.medium .chart { border:1px solid #f9cd0b; }
+/* light yellow */
+.medium { background: #fff4c2; }
+
+.cstat-skip { background: #ddd; color: #111; }
+.fstat-skip { background: #ddd; color: #111 !important; }
+.cbranch-skip { background: #ddd !important; color: #111; }
+
+span.cline-neutral { background: #eaeaea; }
+
+.coverage-summary td.empty {
+ opacity: .5;
+ padding-top: 4px;
+ padding-bottom: 4px;
+ line-height: 1;
+ color: #888;
+}
+
+.cover-fill, .cover-empty {
+ display:inline-block;
+ height: 12px;
+}
+.chart {
+ line-height: 0;
+}
+.cover-empty {
+ background: white;
+}
+.cover-full {
+ border-right: none !important;
+}
+pre.prettyprint {
+ border: none !important;
+ padding: 0 !important;
+ margin: 0 !important;
+}
+.com { color: #999 !important; }
+.ignore-none { color: #999; font-weight: normal; }
+
+.wrapper {
+ min-height: 100%;
+ height: auto !important;
+ height: 100%;
+ margin: 0 auto -48px;
+}
+.footer, .push {
+ height: 48px;
+}
diff --git a/musango-app/coverage/block-navigation.js b/musango-app/coverage/block-navigation.js
new file mode 100644
index 0000000..cc12130
--- /dev/null
+++ b/musango-app/coverage/block-navigation.js
@@ -0,0 +1,87 @@
+/* eslint-disable */
+var jumpToCode = (function init() {
+ // Classes of code we would like to highlight in the file view
+ var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
+
+ // Elements to highlight in the file listing view
+ var fileListingElements = ['td.pct.low'];
+
+ // We don't want to select elements that are direct descendants of another match
+ var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
+
+ // Selecter that finds elements on the page to which we can jump
+ var selector =
+ fileListingElements.join(', ') +
+ ', ' +
+ notSelector +
+ missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
+
+ // The NodeList of matching elements
+ var missingCoverageElements = document.querySelectorAll(selector);
+
+ var currentIndex;
+
+ function toggleClass(index) {
+ missingCoverageElements
+ .item(currentIndex)
+ .classList.remove('highlighted');
+ missingCoverageElements.item(index).classList.add('highlighted');
+ }
+
+ function makeCurrent(index) {
+ toggleClass(index);
+ currentIndex = index;
+ missingCoverageElements.item(index).scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ inline: 'center'
+ });
+ }
+
+ function goToPrevious() {
+ var nextIndex = 0;
+ if (typeof currentIndex !== 'number' || currentIndex === 0) {
+ nextIndex = missingCoverageElements.length - 1;
+ } else if (missingCoverageElements.length > 1) {
+ nextIndex = currentIndex - 1;
+ }
+
+ makeCurrent(nextIndex);
+ }
+
+ function goToNext() {
+ var nextIndex = 0;
+
+ if (
+ typeof currentIndex === 'number' &&
+ currentIndex < missingCoverageElements.length - 1
+ ) {
+ nextIndex = currentIndex + 1;
+ }
+
+ makeCurrent(nextIndex);
+ }
+
+ return function jump(event) {
+ if (
+ document.getElementById('fileSearch') === document.activeElement &&
+ document.activeElement != null
+ ) {
+ // if we're currently focused on the search input, we don't want to navigate
+ return;
+ }
+
+ switch (event.which) {
+ case 78: // n
+ case 74: // j
+ goToNext();
+ break;
+ case 66: // b
+ case 75: // k
+ case 80: // p
+ goToPrevious();
+ break;
+ }
+ };
+})();
+window.addEventListener('keydown', jumpToCode);
diff --git a/musango-app/coverage/favicon.png b/musango-app/coverage/favicon.png
new file mode 100644
index 0000000..c1525b8
Binary files /dev/null and b/musango-app/coverage/favicon.png differ
diff --git a/musango-app/coverage/index.html b/musango-app/coverage/index.html
new file mode 100644
index 0000000..ab56669
--- /dev/null
+++ b/musango-app/coverage/index.html
@@ -0,0 +1,161 @@
+
+
+
+
+
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| musango-app | +
+
+ |
+ 70% | +21/30 | +37.5% | +3/8 | +20% | +1/5 | +70% | +21/30 | +
| musango-app/models | +
+
+ |
+ 100% | +3/3 | +100% | +0/0 | +100% | +0/0 | +100% | +3/3 | +
| musango-app/routes | +
+
+ |
+ 81.66% | +49/60 | +82.14% | +23/28 | +75% | +6/8 | +81.66% | +49/60 | +
| musango-app/utils | +
+
+ |
+ 90.47% | +19/21 | +50% | +2/4 | +50% | +1/2 | +90.47% | +19/21 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| musango-app | +
+
+ |
+ 70% | +21/30 | +37.5% | +3/8 | +20% | +1/5 | +70% | +21/30 | +
| musango-app/models | +
+
+ |
+ 100% | +3/3 | +100% | +0/0 | +100% | +0/0 | +100% | +3/3 | +
| musango-app/routes | +
+
+ |
+ 81.66% | +49/60 | +82.14% | +23/28 | +75% | +6/8 | +81.66% | +49/60 | +
| musango-app/utils | +
+
+ |
+ 90.47% | +19/21 | +50% | +2/4 | +50% | +1/2 | +90.47% | +19/21 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 | +2x +2x +2x +2x +2x + +2x +2x +2x + + +2x + + +2x +2x +2x +2x + + +2x +2x + + +2x +2x + + +2x + + + +2x + + +2x + + + + + + + + + + + + + +2x + | // app.js
+const express = require('express');
+const mongoose = require('mongoose');
+const path = require('path');
+const dotenv = require('dotenv');
+const morgan = require('morgan');
+
+dotenv.config();
+const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/musango-express';
+const PORT = process.env.PORT || 8080;
+
+function createServer() {
+ const app = express();
+
+ // Middleware
+ app.use(express.urlencoded({ extended: true }));
+ app.use(express.json());
+ app.use(express.static(path.join(__dirname, 'public')));
+ app.use(morgan('dev'));
+
+ // View engine
+ app.set('view engine', 'ejs');
+ app.set('views', path.join(__dirname, 'views'));
+
+ // Routes
+ app.use('/', require('./routes/index'));
+ app.use('/', require('./routes/booking'));
+
+ // Health check
+ app.get('/health', (req, res) => {
+ res.status(200).json({ status: 'ok', env: process.env.NODE_ENV || 'dev' });
+ });
+
+ return app;
+}
+
+Iif (require.main === module) {
+ mongoose.set('strictQuery', false);
+ mongoose.connect(MONGO_URI).then(() => {
+ console.log('Connected to MongoDB');
+ const app = createServer();
+ app.listen(PORT, () => {
+ console.log(`π Musango App is running at http://localhost:${PORT}`);
+ });
+ }).catch(err => {
+ console.error('MongoDB connection error:', err);
+ process.exit(1);
+ });
+}
+
+module.exports = { createServer };
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| app.js | +
+
+ |
+ 70% | +21/30 | +37.5% | +3/8 | +20% | +1/5 | +70% | +21/30 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 | 2x + +2x + + + + + + + + + + + +2x | const mongoose = require('mongoose');
+
+const bookingSchema = new mongoose.Schema({
+ destination: { type: String, required: true },
+ name: { type: String, required: true },
+ age: { type: Number, required: true },
+ contact: { type: String, required: true },
+ date: { type: String, required: true },
+ time: { type: String, required: true },
+ busSerial: { type: String, required: true },
+ receiptNumber: { type: String, required: true },
+ createdAt: { type: Date, default: Date.now }
+});
+
+module.exports = mongoose.model('Booking', bookingSchema); |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| booking.js | +
+
+ |
+ 100% | +3/3 | +100% | +0/0 | +100% | +0/0 | +100% | +3/3 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 | 2x +2x +2x +2x +2x +2x +2x + + +2x +2x +2x +1x + + +1x + + + + + + + +2x +5x + +5x + +5x +5x + + +5x +2x + + + + + + +3x +1x + + + + + + +2x +1x + + + + + + +1x + + + + + + + +1x + +1x +1x + + + + + + + + + + + +1x + + +1x + + +1x +1x + + + +1x +1x + + +1x + + + + + +1x + + + + + + + + + + + + + + + + +2x + | const express = require('express');
+const router = express.Router();
+const Booking = require('../models/booking');
+const { generateReceiptPDF } = require('../utils/pdfGenerator');
+const sendEmailWithPDF = require('../utils/mailer');
+const fs = require('fs');
+const path = require('path');
+
+// Render booking form
+router.get('/booking', (req, res) => {
+ const { destination } = req.query;
+ if (!destination) {
+ return res.status(400).send('Destination is required.');
+ }
+
+ res.render('booking-form', {
+ destination,
+ error: null,
+ formData: {}
+ });
+});
+
+// Handle booking form submission
+router.post('/book', async (req, res) => {
+ console.log('π₯ Form submission body:', req.body);
+
+ const { destination, name, age, contact, email, date, time, busSerial } = req.body;
+
+ const formData = { name, age, contact, email, date, time, busSerial };
+ const safeDestination = destination || '';
+
+ // Basic validation
+ if (!destination || !name || !age || !contact || !email || !date || !time || !busSerial) {
+ return res.status(400).render('booking-form', {
+ destination: safeDestination,
+ error: 'All fields are required.',
+ formData
+ });
+ }
+
+ if (!email.endsWith('@gmail.com')) {
+ return res.status(400).render('booking-form', {
+ destination: safeDestination,
+ error: 'Only Gmail addresses are accepted.',
+ formData
+ });
+ }
+
+ if (isNaN(age)) {
+ return res.status(400).render('booking-form', {
+ destination: safeDestination,
+ error: 'Age must be a number.',
+ formData
+ });
+ }
+
+ Iif (contact.length < 10 || contact.length > 15) {
+ return res.status(400).render('booking-form', {
+ destination: safeDestination,
+ error: 'Contact number must be between 10 and 15 characters.',
+ formData
+ });
+ }
+
+ const receiptNumber = `REC-${Math.floor(Math.random() * 1000000).toString().padStart(6, '0')}`;
+
+ try {
+ const booking = new Booking({
+ destination: safeDestination,
+ name,
+ age,
+ contact,
+ email,
+ date,
+ time,
+ busSerial,
+ receiptNumber
+ });
+
+ await booking.save();
+
+ // Generate receipt PDF
+ const pdfBuffer = await generateReceiptPDF(booking);
+
+ // Save locally for download
+ const receiptsDir = path.join(__dirname, '..', 'public', 'receipts');
+ Iif (!fs.existsSync(receiptsDir)) {
+ fs.mkdirSync(receiptsDir, { recursive: true });
+ }
+
+ const pdfPath = path.join(receiptsDir, `${receiptNumber}.pdf`);
+ fs.writeFileSync(pdfPath, pdfBuffer);
+
+ // Send email to client only (skip in test environment)
+ Iif (process.env.NODE_ENV !== 'test') {
+ console.log(`π€ Sending receipt to user: ${email}`);
+ await sendEmailWithPDF(email, pdfBuffer, booking);
+ }
+
+ // Show success page with download option
+ res.render('booking-success', { booking });
+
+ } catch (err) {
+ console.error('Booking error:', err);
+
+ if (err.code === 11000) {
+ return res.status(400).render('booking-form', {
+ destination: safeDestination,
+ error: 'Duplicate booking detected. Please try again.',
+ formData
+ });
+ }
+
+ res.status(500).render('error', { message: 'Internal Server Error' });
+ }
+});
+
+module.exports = router;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| booking.js | +
+
+ |
+ 81.81% | +36/44 | +82.14% | +23/28 | +100% | +2/2 | +81.81% | +36/44 | +
| index.js | +
+
+ |
+ 81.25% | +13/16 | +100% | +0/0 | +66.66% | +4/6 | +81.25% | +13/16 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 | 2x +2x + +2x +1x + + +2x +1x + + +2x +1x + + +2x + + + +2x + + + + + + + + + +2x +1x + + +2x + | const express = require('express');
+const router = express.Router();
+
+router.get('/', (req, res) => {
+ res.render('index', { title: 'Home' });
+});
+
+router.get('/about', (req, res) => {
+ res.render('about', { title: 'About Us' });
+});
+
+router.get('/services', (req, res) => {
+ res.render('services', { title: 'Our Services' });
+});
+
+router.get('/contacts', (req, res) => {
+ res.render('contacts', { title: 'Contact Us' });
+});
+
+router.get('/destinations', (req, res) => {
+ const regions = ['Littoral', 'Centre', 'East', 'Northwest', 'West', 'South', 'Adamawa', 'FarNorth', 'North', 'Southwest']; // You can update this list
+ res.render('destinations', {
+ title: 'Our Destinations',
+ regions
+ });
+});
+
+
+// Optional health check for testing
+router.get('/health', (req, res) => {
+ res.json({ status: 'OK' });
+});
+
+module.exports = router;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| mailer.js | +
+
+ |
+ 60% | +3/5 | +50% | +2/4 | +0% | +0/1 | +60% | +3/5 | +
| pdfGenerator.js | +
+
+ |
+ 100% | +16/16 | +100% | +0/0 | +100% | +1/1 | +100% | +16/16 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 | +2x + + +2x + + + + + + + + + + + + + + + + + + + + + + + + + +2x + | // utils/mailer.js
+const nodemailer = require('nodemailer');
+
+// Update this with your credentials or use environment variables
+const transporter = nodemailer.createTransport({
+ service: 'gmail',
+ auth: {
+ user: process.env.MAIL_USER || 'your-email@gmail.com',
+ pass: process.env.MAIL_PASS || 'your-app-password'
+ }
+});
+
+async function sendEmailWithPDF(to, pdfBuffer, booking) {
+ const mailOptions = {
+ from: '"Hill-Top Consultancy" <your-email@gmail.com>',
+ to,
+ subject: `Booking Receipt - ${booking.receiptNumber}`,
+ text: `Hello ${booking.name},\n\nThank you for booking with Hill-Top Consultancy. Your receipt is attached.\n\nDestination: ${booking.destination}\nDate: ${booking.date}\nTotal Paid: 12,400 XAF\n\nSafe travels!`,
+ attachments: [
+ {
+ filename: `receipt-${booking.receiptNumber}.pdf`,
+ content: pdfBuffer,
+ contentType: 'application/pdf'
+ }
+ ]
+ };
+
+ await transporter.sendMail(mailOptions);
+}
+
+module.exports = sendEmailWithPDF;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 | 2x +2x +2x +2x + + +1x + + +1x +1x +1x + + +1x + + + + + +1x +1x +1x + +1x + + + + + +1x +1x + + +2x + | const ejs = require('ejs');
+const puppeteer = require('puppeteer');
+const path = require('path');
+const fs = require('fs');
+
+async function generateReceiptPDF(booking) {
+ const ejsPath = path.join(__dirname, '..', 'views', 'receipt.ejs');
+
+ // β
Convert logo image to Base64
+ const logoPath = path.join(__dirname, '..', 'public', 'images', 'logo.png');
+ const logoBase64 = fs.readFileSync(logoPath, 'base64');
+ const logoDataURI = `data:image/png;base64,${logoBase64}`;
+
+ // β
Render HTML with booking data and embedded logo
+ const html = await ejs.renderFile(ejsPath, {
+ booking,
+ logoDataURI
+ });
+
+ // Launch Puppeteer and create PDF
+ const browser = await puppeteer.launch({ headless: 'new' });
+ const page = await browser.newPage();
+ await page.setContent(html, { waitUntil: 'networkidle0' });
+
+ const pdfBuffer = await page.pdf({
+ format: 'A4',
+ printBackground: true,
+ margin: { top: '20mm', bottom: '20mm', left: '10mm', right: '10mm' }
+ });
+
+ await browser.close();
+ return pdfBuffer;
+}
+
+module.exports = { generateReceiptPDF };
+ |