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 + +![Node.js](https://img.shields.io/badge/Node.js-18.0+-339933?logo=node.js&logoColor=white) +![Express.js](https://img.shields.io/badge/Express.js-4.18.0-000000?logo=express&logoColor=white) +![MongoDB](https://img.shields.io/badge/MongoDB-6.0+-47A248?logo=mongodb&logoColor=white) +![Docker](https://img.shields.io/badge/Docker-Containerized-2496ED?logo=docker&logoColor=white) +![Kubernetes](https://img.shields.io/badge/Kubernetes-1.28+-326CE5?logo=kubernetes&logoColor=white) +![CircleCI](https://img.shields.io/badge/CircleCI-2.1+-343434?logo=circleci&logoColor=white) +![License](https://img.shields.io/badge/License-MIT-green) + +## 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. + +image + +## πŸ—οΈ 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) + +[![LinkedIn](https://img.shields.io/badge/LinkedIn-Connect-blue?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/sani-chafah/) +[![GitHub](https://img.shields.io/badge/GitHub-Follow-black?logo=github&logoColor=white)](https://github.com/CHAFAH) +[![Portfolio](https://img.shields.io/badge/Portfolio-Visit-green?logo=react&logoColor=white)](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 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 80.7% + Statements + 92/114 +
+ + +
+ 70% + Branches + 28/40 +
+ + +
+ 53.33% + Functions + 8/15 +
+ + +
+ 80.7% + Lines + 92/114 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
musango-app +
+
70%21/3037.5%3/820%1/570%21/30
musango-app/models +
+
100%3/3100%0/0100%0/0100%3/3
musango-app/routes +
+
81.66%49/6082.14%23/2875%6/881.66%49/60
musango-app/utils +
+
90.47%19/2150%2/450%1/290.47%19/21
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/lcov-report/base.css b/musango-app/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/musango-app/coverage/lcov-report/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/lcov-report/block-navigation.js b/musango-app/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..cc12130 --- /dev/null +++ b/musango-app/coverage/lcov-report/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/lcov-report/favicon.png b/musango-app/coverage/lcov-report/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/musango-app/coverage/lcov-report/favicon.png differ diff --git a/musango-app/coverage/lcov-report/index.html b/musango-app/coverage/lcov-report/index.html new file mode 100644 index 0000000..5764f31 --- /dev/null +++ b/musango-app/coverage/lcov-report/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 80.7% + Statements + 92/114 +
+ + +
+ 70% + Branches + 28/40 +
+ + +
+ 53.33% + Functions + 8/15 +
+ + +
+ 80.7% + Lines + 92/114 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
musango-app +
+
70%21/3037.5%3/820%1/570%21/30
musango-app/models +
+
100%3/3100%0/0100%0/0100%3/3
musango-app/routes +
+
81.66%49/6082.14%23/2875%6/881.66%49/60
musango-app/utils +
+
90.47%19/2150%2/450%1/290.47%19/21
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/lcov-report/musango-app/app.js.html b/musango-app/coverage/lcov-report/musango-app/app.js.html new file mode 100644 index 0000000..e610a4e --- /dev/null +++ b/musango-app/coverage/lcov-report/musango-app/app.js.html @@ -0,0 +1,238 @@ + + + + + + Code coverage report for musango-app/app.js + + + + + + + + + +
+
+

All files / musango-app app.js

+
+ +
+ 70% + Statements + 21/30 +
+ + +
+ 37.5% + Branches + 3/8 +
+ + +
+ 20% + Functions + 1/5 +
+ + +
+ 70% + Lines + 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 +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 };
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/lcov-report/musango-app/index.html b/musango-app/coverage/lcov-report/musango-app/index.html new file mode 100644 index 0000000..6c48978 --- /dev/null +++ b/musango-app/coverage/lcov-report/musango-app/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for musango-app + + + + + + + + + +
+
+

All files musango-app

+
+ +
+ 70% + Statements + 21/30 +
+ + +
+ 37.5% + Branches + 3/8 +
+ + +
+ 20% + Functions + 1/5 +
+ + +
+ 70% + Lines + 21/30 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
app.js +
+
70%21/3037.5%3/820%1/570%21/30
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/lcov-report/musango-app/models/booking.js.html b/musango-app/coverage/lcov-report/musango-app/models/booking.js.html new file mode 100644 index 0000000..3e7a789 --- /dev/null +++ b/musango-app/coverage/lcov-report/musango-app/models/booking.js.html @@ -0,0 +1,127 @@ + + + + + + Code coverage report for musango-app/models/booking.js + + + + + + + + + +
+
+

All files / musango-app/models booking.js

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 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 +152x +  +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);
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/lcov-report/musango-app/models/index.html b/musango-app/coverage/lcov-report/musango-app/models/index.html new file mode 100644 index 0000000..6567ac9 --- /dev/null +++ b/musango-app/coverage/lcov-report/musango-app/models/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for musango-app/models + + + + + + + + + +
+
+

All files musango-app/models

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
booking.js +
+
100%3/3100%0/0100%0/0100%3/3
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/lcov-report/musango-app/routes/booking.js.html b/musango-app/coverage/lcov-report/musango-app/routes/booking.js.html new file mode 100644 index 0000000..e21343d --- /dev/null +++ b/musango-app/coverage/lcov-report/musango-app/routes/booking.js.html @@ -0,0 +1,439 @@ + + + + + + Code coverage report for musango-app/routes/booking.js + + + + + + + + + +
+
+

All files / musango-app/routes booking.js

+
+ +
+ 81.81% + Statements + 36/44 +
+ + +
+ 82.14% + Branches + 23/28 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 81.81% + Lines + 36/44 +
+ + +
+

+ 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 +1192x +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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/lcov-report/musango-app/routes/index.html b/musango-app/coverage/lcov-report/musango-app/routes/index.html new file mode 100644 index 0000000..ede7182 --- /dev/null +++ b/musango-app/coverage/lcov-report/musango-app/routes/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for musango-app/routes + + + + + + + + + +
+
+

All files musango-app/routes

+
+ +
+ 81.66% + Statements + 49/60 +
+ + +
+ 82.14% + Branches + 23/28 +
+ + +
+ 75% + Functions + 6/8 +
+ + +
+ 81.66% + Lines + 49/60 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
booking.js +
+
81.81%36/4482.14%23/28100%2/281.81%36/44
index.js +
+
81.25%13/16100%0/066.66%4/681.25%13/16
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/lcov-report/musango-app/routes/index.js.html b/musango-app/coverage/lcov-report/musango-app/routes/index.js.html new file mode 100644 index 0000000..932c558 --- /dev/null +++ b/musango-app/coverage/lcov-report/musango-app/routes/index.js.html @@ -0,0 +1,187 @@ + + + + + + Code coverage report for musango-app/routes/index.js + + + + + + + + + +
+
+

All files / musango-app/routes index.js

+
+ +
+ 81.25% + Statements + 13/16 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 66.66% + Functions + 4/6 +
+ + +
+ 81.25% + Lines + 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 +352x +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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/lcov-report/musango-app/utils/index.html b/musango-app/coverage/lcov-report/musango-app/utils/index.html new file mode 100644 index 0000000..b4682c5 --- /dev/null +++ b/musango-app/coverage/lcov-report/musango-app/utils/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for musango-app/utils + + + + + + + + + +
+
+

All files musango-app/utils

+
+ +
+ 90.47% + Statements + 19/21 +
+ + +
+ 50% + Branches + 2/4 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 90.47% + Lines + 19/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
mailer.js +
+
60%3/550%2/40%0/160%3/5
pdfGenerator.js +
+
100%16/16100%0/0100%1/1100%16/16
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/lcov-report/musango-app/utils/mailer.js.html b/musango-app/coverage/lcov-report/musango-app/utils/mailer.js.html new file mode 100644 index 0000000..32fad10 --- /dev/null +++ b/musango-app/coverage/lcov-report/musango-app/utils/mailer.js.html @@ -0,0 +1,178 @@ + + + + + + Code coverage report for musango-app/utils/mailer.js + + + + + + + + + +
+
+

All files / musango-app/utils mailer.js

+
+ +
+ 60% + Statements + 3/5 +
+ + +
+ 50% + Branches + 2/4 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 60% + Lines + 3/5 +
+ + +
+

+ 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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/lcov-report/musango-app/utils/pdfGenerator.js.html b/musango-app/coverage/lcov-report/musango-app/utils/pdfGenerator.js.html new file mode 100644 index 0000000..039fb20 --- /dev/null +++ b/musango-app/coverage/lcov-report/musango-app/utils/pdfGenerator.js.html @@ -0,0 +1,190 @@ + + + + + + Code coverage report for musango-app/utils/pdfGenerator.js + + + + + + + + + +
+
+

All files / musango-app/utils pdfGenerator.js

+
+ +
+ 100% + Statements + 16/16 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 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 +33 +34 +35 +362x +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 };
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/lcov-report/prettify.css b/musango-app/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/musango-app/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/musango-app/coverage/lcov-report/prettify.js b/musango-app/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/musango-app/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/musango-app/coverage/lcov-report/sort-arrow-sprite.png b/musango-app/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000..6ed6831 Binary files /dev/null and b/musango-app/coverage/lcov-report/sort-arrow-sprite.png differ diff --git a/musango-app/coverage/lcov-report/sorter.js b/musango-app/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..2bb296a --- /dev/null +++ b/musango-app/coverage/lcov-report/sorter.js @@ -0,0 +1,196 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if ( + row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()) + ) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/musango-app/coverage/lcov.info b/musango-app/coverage/lcov.info new file mode 100644 index 0000000..bb7acea --- /dev/null +++ b/musango-app/coverage/lcov.info @@ -0,0 +1,238 @@ +TN: +SF:app.js +FN:12,createServer +FN:30,(anonymous_1) +FN:39,(anonymous_2) +FN:42,(anonymous_3) +FN:45,(anonymous_4) +FNF:5 +FNH:1 +FNDA:2,createServer +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +DA:2,2 +DA:3,2 +DA:4,2 +DA:5,2 +DA:6,2 +DA:8,2 +DA:9,2 +DA:10,2 +DA:13,2 +DA:16,2 +DA:17,2 +DA:18,2 +DA:19,2 +DA:22,2 +DA:23,2 +DA:26,2 +DA:27,2 +DA:30,2 +DA:31,0 +DA:34,2 +DA:37,2 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:46,0 +DA:47,0 +DA:51,2 +LF:30 +LH:21 +BRDA:9,0,0,2 +BRDA:9,0,1,0 +BRDA:10,1,0,2 +BRDA:10,1,1,0 +BRDA:31,2,0,0 +BRDA:31,2,1,0 +BRDA:37,3,0,0 +BRDA:37,3,1,2 +BRF:8 +BRH:3 +end_of_record +TN: +SF:models\booking.js +FNF:0 +FNH:0 +DA:1,2 +DA:3,2 +DA:15,2 +LF:3 +LH:3 +BRF:0 +BRH:0 +end_of_record +TN: +SF:routes\booking.js +FN:10,(anonymous_0) +FN:24,(anonymous_1) +FNF:2 +FNH:2 +FNDA:2,(anonymous_0) +FNDA:5,(anonymous_1) +DA:1,2 +DA:2,2 +DA:3,2 +DA:4,2 +DA:5,2 +DA:6,2 +DA:7,2 +DA:10,2 +DA:11,2 +DA:12,2 +DA:13,1 +DA:16,1 +DA:24,2 +DA:25,5 +DA:27,5 +DA:29,5 +DA:30,5 +DA:33,5 +DA:34,2 +DA:41,3 +DA:42,1 +DA:49,2 +DA:50,1 +DA:57,1 +DA:58,0 +DA:65,1 +DA:67,1 +DA:68,1 +DA:80,1 +DA:83,1 +DA:86,1 +DA:87,1 +DA:88,0 +DA:91,1 +DA:92,1 +DA:95,1 +DA:96,0 +DA:97,0 +DA:101,1 +DA:104,0 +DA:106,0 +DA:107,0 +DA:114,0 +DA:118,2 +LF:44 +LH:36 +BRDA:12,0,0,1 +BRDA:12,0,1,1 +BRDA:30,1,0,5 +BRDA:30,1,1,1 +BRDA:33,2,0,2 +BRDA:33,2,1,3 +BRDA:33,3,0,5 +BRDA:33,3,1,4 +BRDA:33,3,2,4 +BRDA:33,3,3,4 +BRDA:33,3,4,4 +BRDA:33,3,5,3 +BRDA:33,3,6,3 +BRDA:33,3,7,3 +BRDA:41,4,0,1 +BRDA:41,4,1,2 +BRDA:49,5,0,1 +BRDA:49,5,1,1 +BRDA:57,6,0,0 +BRDA:57,6,1,1 +BRDA:57,7,0,1 +BRDA:57,7,1,1 +BRDA:87,8,0,0 +BRDA:87,8,1,1 +BRDA:95,9,0,0 +BRDA:95,9,1,1 +BRDA:106,10,0,0 +BRDA:106,10,1,0 +BRF:28 +BRH:23 +end_of_record +TN: +SF:routes\index.js +FN:4,(anonymous_0) +FN:8,(anonymous_1) +FN:12,(anonymous_2) +FN:16,(anonymous_3) +FN:20,(anonymous_4) +FN:30,(anonymous_5) +FNF:6 +FNH:4 +FNDA:1,(anonymous_0) +FNDA:1,(anonymous_1) +FNDA:1,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:1,(anonymous_5) +DA:1,2 +DA:2,2 +DA:4,2 +DA:5,1 +DA:8,2 +DA:9,1 +DA:12,2 +DA:13,1 +DA:16,2 +DA:17,0 +DA:20,2 +DA:21,0 +DA:22,0 +DA:30,2 +DA:31,1 +DA:34,2 +LF:16 +LH:13 +BRF:0 +BRH:0 +end_of_record +TN: +SF:utils\mailer.js +FN:13,sendEmailWithPDF +FNF:1 +FNH:0 +FNDA:0,sendEmailWithPDF +DA:2,2 +DA:5,2 +DA:14,0 +DA:28,0 +DA:31,2 +LF:5 +LH:3 +BRDA:8,0,0,2 +BRDA:8,0,1,0 +BRDA:9,1,0,2 +BRDA:9,1,1,0 +BRF:4 +BRH:2 +end_of_record +TN: +SF:utils\pdfGenerator.js +FN:6,generateReceiptPDF +FNF:1 +FNH:1 +FNDA:1,generateReceiptPDF +DA:1,2 +DA:2,2 +DA:3,2 +DA:4,2 +DA:7,1 +DA:10,1 +DA:11,1 +DA:12,1 +DA:15,1 +DA:21,1 +DA:22,1 +DA:23,1 +DA:25,1 +DA:31,1 +DA:32,1 +DA:35,2 +LF:16 +LH:16 +BRF:0 +BRH:0 +end_of_record diff --git a/musango-app/coverage/musango-app/app.js.html b/musango-app/coverage/musango-app/app.js.html new file mode 100644 index 0000000..7d6c40d --- /dev/null +++ b/musango-app/coverage/musango-app/app.js.html @@ -0,0 +1,238 @@ + + + + + + Code coverage report for musango-app/app.js + + + + + + + + + +
+
+

All files / musango-app app.js

+
+ +
+ 70% + Statements + 21/30 +
+ + +
+ 37.5% + Branches + 3/8 +
+ + +
+ 20% + Functions + 1/5 +
+ + +
+ 70% + Lines + 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 +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 };
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/musango-app/index.html b/musango-app/coverage/musango-app/index.html new file mode 100644 index 0000000..1859dcc --- /dev/null +++ b/musango-app/coverage/musango-app/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for musango-app + + + + + + + + + +
+
+

All files musango-app

+
+ +
+ 70% + Statements + 21/30 +
+ + +
+ 37.5% + Branches + 3/8 +
+ + +
+ 20% + Functions + 1/5 +
+ + +
+ 70% + Lines + 21/30 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
app.js +
+
70%21/3037.5%3/820%1/570%21/30
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/musango-app/models/booking.js.html b/musango-app/coverage/musango-app/models/booking.js.html new file mode 100644 index 0000000..67f7cfe --- /dev/null +++ b/musango-app/coverage/musango-app/models/booking.js.html @@ -0,0 +1,127 @@ + + + + + + Code coverage report for musango-app/models/booking.js + + + + + + + + + +
+
+

All files / musango-app/models booking.js

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 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 +152x +  +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);
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/musango-app/models/index.html b/musango-app/coverage/musango-app/models/index.html new file mode 100644 index 0000000..e7e774a --- /dev/null +++ b/musango-app/coverage/musango-app/models/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for musango-app/models + + + + + + + + + +
+
+

All files musango-app/models

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
booking.js +
+
100%3/3100%0/0100%0/0100%3/3
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/musango-app/routes/booking.js.html b/musango-app/coverage/musango-app/routes/booking.js.html new file mode 100644 index 0000000..4273125 --- /dev/null +++ b/musango-app/coverage/musango-app/routes/booking.js.html @@ -0,0 +1,439 @@ + + + + + + Code coverage report for musango-app/routes/booking.js + + + + + + + + + +
+
+

All files / musango-app/routes booking.js

+
+ +
+ 81.81% + Statements + 36/44 +
+ + +
+ 82.14% + Branches + 23/28 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 81.81% + Lines + 36/44 +
+ + +
+

+ 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 +1192x +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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/musango-app/routes/index.html b/musango-app/coverage/musango-app/routes/index.html new file mode 100644 index 0000000..4a76675 --- /dev/null +++ b/musango-app/coverage/musango-app/routes/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for musango-app/routes + + + + + + + + + +
+
+

All files musango-app/routes

+
+ +
+ 81.66% + Statements + 49/60 +
+ + +
+ 82.14% + Branches + 23/28 +
+ + +
+ 75% + Functions + 6/8 +
+ + +
+ 81.66% + Lines + 49/60 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
booking.js +
+
81.81%36/4482.14%23/28100%2/281.81%36/44
index.js +
+
81.25%13/16100%0/066.66%4/681.25%13/16
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/musango-app/routes/index.js.html b/musango-app/coverage/musango-app/routes/index.js.html new file mode 100644 index 0000000..d23ca9e --- /dev/null +++ b/musango-app/coverage/musango-app/routes/index.js.html @@ -0,0 +1,187 @@ + + + + + + Code coverage report for musango-app/routes/index.js + + + + + + + + + +
+
+

All files / musango-app/routes index.js

+
+ +
+ 81.25% + Statements + 13/16 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 66.66% + Functions + 4/6 +
+ + +
+ 81.25% + Lines + 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 +352x +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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/musango-app/utils/index.html b/musango-app/coverage/musango-app/utils/index.html new file mode 100644 index 0000000..57e6952 --- /dev/null +++ b/musango-app/coverage/musango-app/utils/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for musango-app/utils + + + + + + + + + +
+
+

All files musango-app/utils

+
+ +
+ 90.47% + Statements + 19/21 +
+ + +
+ 50% + Branches + 2/4 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 90.47% + Lines + 19/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
mailer.js +
+
60%3/550%2/40%0/160%3/5
pdfGenerator.js +
+
100%16/16100%0/0100%1/1100%16/16
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/musango-app/utils/mailer.js.html b/musango-app/coverage/musango-app/utils/mailer.js.html new file mode 100644 index 0000000..10e3a8f --- /dev/null +++ b/musango-app/coverage/musango-app/utils/mailer.js.html @@ -0,0 +1,178 @@ + + + + + + Code coverage report for musango-app/utils/mailer.js + + + + + + + + + +
+
+

All files / musango-app/utils mailer.js

+
+ +
+ 60% + Statements + 3/5 +
+ + +
+ 50% + Branches + 2/4 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 60% + Lines + 3/5 +
+ + +
+

+ 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;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/musango-app/utils/pdfGenerator.js.html b/musango-app/coverage/musango-app/utils/pdfGenerator.js.html new file mode 100644 index 0000000..54a2398 --- /dev/null +++ b/musango-app/coverage/musango-app/utils/pdfGenerator.js.html @@ -0,0 +1,190 @@ + + + + + + Code coverage report for musango-app/utils/pdfGenerator.js + + + + + + + + + +
+
+

All files / musango-app/utils pdfGenerator.js

+
+ +
+ 100% + Statements + 16/16 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 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 +33 +34 +35 +362x +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 };
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/musango-app/coverage/prettify.css b/musango-app/coverage/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/musango-app/coverage/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/musango-app/coverage/prettify.js b/musango-app/coverage/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/musango-app/coverage/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/musango-app/coverage/sort-arrow-sprite.png b/musango-app/coverage/sort-arrow-sprite.png new file mode 100644 index 0000000..6ed6831 Binary files /dev/null and b/musango-app/coverage/sort-arrow-sprite.png differ diff --git a/musango-app/coverage/sorter.js b/musango-app/coverage/sorter.js new file mode 100644 index 0000000..2bb296a --- /dev/null +++ b/musango-app/coverage/sorter.js @@ -0,0 +1,196 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if ( + row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()) + ) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/musango-app/kubernetes/deployment/mongo.yaml b/musango-app/kubernetes/deployment/mongo.yaml new file mode 100644 index 0000000..60442a6 --- /dev/null +++ b/musango-app/kubernetes/deployment/mongo.yaml @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mongodb +spec: + replicas: 1 + selector: + matchLabels: + app: mongodb + template: + metadata: + labels: + app: mongodb + spec: + containers: + - name: mongodb + image: mongo:latest + ports: + - containerPort: 27017 + env: + - name: MONGO_INITDB_DATABASE + value: "musango-express" +--- +apiVersion: v1 +kind: Service +metadata: + name: mongodb +spec: + selector: + app: mongodb + ports: + - protocol: TCP + port: 27017 # Service port + targetPort: 27017 # Container port \ No newline at end of file diff --git a/musango-app/kubernetes/deployment/musango.yaml b/musango-app/kubernetes/deployment/musango.yaml new file mode 100644 index 0000000..d98d877 --- /dev/null +++ b/musango-app/kubernetes/deployment/musango.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: musango +spec: + replicas: 1 + selector: + matchLabels: + app: musango + template: + metadata: + labels: + app: musango + spec: + containers: + - name: musango + image: hilltopconsultancy/musango:v1 + ports: + - containerPort: 8082 + env: + - name: MONGO_URI + value: "mongodb://mongodb:27017/musango-express" + - name: PORT + value: "8082" +--- +apiVersion: v1 +kind: Service +metadata: + name: musango +spec: + type: NodePort + selector: + app: musango + ports: + - protocol: TCP + port: 8082 # Service port + targetPort: 8082 # Container port + nodePort: 30012 # External access port \ No newline at end of file diff --git a/musango-app/models/booking.js b/musango-app/models/booking.js new file mode 100644 index 0000000..088dcd7 --- /dev/null +++ b/musango-app/models/booking.js @@ -0,0 +1,15 @@ +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); \ No newline at end of file diff --git a/musango-app/package.json b/musango-app/package.json new file mode 100644 index 0000000..3de97a1 --- /dev/null +++ b/musango-app/package.json @@ -0,0 +1,47 @@ +{ + "name": "musango-express", + "version": "1.0.0", + "description": "Musango Express Ticket Management App", + "main": "app.js", + "scripts": { + "start": "node app.js", + "dev": "nodemon app.js", + "test": "cross-env NODE_ENV=test jest --detectOpenHandles --testTimeout=10000 --verbose", + "test:watch": "cross-env NODE_ENV=test jest --watch", + "test:debug": "node --inspect-brk ./node_modules/.bin/jest --runInBand", + "lint": "eslint .", + "build": "echo 'No build step for this app'" + }, + "dependencies": { + "dotenv": "^10.0.0", + "ejs": "^3.1.10", + "express": "^4.17.1", + "mongoose": "^6.0.12", + "morgan": "^1.10.0", + "nodemailer": "^6.10.1", + "puppeteer": "^24.7.0" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "cross-env": "^7.0.3", + "eslint": "^8.57.1", + "eslint-plugin-react": "^7.37.5", + "globals": "^16.0.0", + "jest": "^29.0.0", + "nodemon": "^2.0.22", + "prettier": "^3.5.3", + "supertest": "^6.3.3" + }, + "jest": { + "testEnvironment": "node", + "setupFilesAfterEnv": [ + "./test/setup.js" + ], + "collectCoverage": true, + "coverageReporters": [ + "text", + "html", + "lcov" + ] + } +} diff --git a/musango-app/public/css/styles.css b/musango-app/public/css/styles.css new file mode 100644 index 0000000..6c112fb --- /dev/null +++ b/musango-app/public/css/styles.css @@ -0,0 +1,299 @@ +/* General Styles */ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f4f4f4; + color: #333; + line-height: 1.6; +} + +/* Header and Menu Bar */ +header { + background-color: #003366; /* Dark blue */ + padding: 20px 0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +header .container { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +header .logo { + height: 80px; /* Larger logo */ +} + +header nav ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; +} + +header nav ul li { + margin-left: 20px; +} + +header nav ul li a { + color: white; /* White text for contrast */ + text-decoration: none; + font-weight: bold; + font-size: 18px; /* Larger font size */ + transition: color 0.3s ease; +} + +header nav ul li a:hover { + color: #ffcc00; /* Yellow on hover */ +} + +/* Main Content */ +main.content { + background-color: white; + padding: 40px 20px; + max-width: 1200px; + margin: 20px auto; + border-radius: 10px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +main.content h1 { + font-size: 36px; /* Larger font size */ + color: #003366; + margin-bottom: 20px; + text-align: center; +} + +main.content h2 { + font-size: 28px; + color: #003366; + margin-bottom: 15px; +} + +main.content p { + font-size: 18px; /* Larger font size */ + color: #333; + line-height: 1.6; + margin-bottom: 20px; +} + +main.content ul { + text-align: left; + margin-bottom: 20px; + padding-left: 20px; +} + +main.content ul li { + font-size: 18px; /* Larger font size */ + color: #333; + margin-bottom: 10px; +} + +/* Hero Section */ +.hero { + text-align: center; + padding: 4rem 2rem; + background-color: #003366; + color: white; + border-radius: 10px; + margin-bottom: 40px; +} + +.hero h1 { + font-size: 2.5rem; + margin-bottom: 1rem; + color: white; +} + +.hero p { + font-size: 1.25rem; + margin-bottom: 2rem; +} + +.hero .cta-button { + background-color: #ffcc00; + color: #003366; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + text-decoration: none; + font-size: 1rem; + transition: background-color 0.3s ease; +} + +.hero .cta-button:hover { + background-color: #e6b800; +} + +/* Features Section */ +.features { + padding: 2rem; + text-align: center; +} + +.features h2 { + font-size: 2rem; + margin-bottom: 2rem; +} + +.feature-list { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; +} + +.feature-item { + flex: 1 1 calc(33.333% - 2rem); + padding: 1.5rem; + border: 1px solid #ddd; + border-radius: 8px; + background-color: #f9f9f9; + text-align: center; +} + +.feature-item h3 { + font-size: 1.5rem; + margin-bottom: 1rem; + color: #003366; +} + +.feature-item p { + font-size: 1rem; + color: #333; +} + +/* Services Section */ +.services { + display: flex; + flex-wrap: wrap; + gap: 2rem; + margin: 2rem 0; +} + +.service { + flex: 1 1 calc(33.333% - 2rem); + padding: 1.5rem; + border: 1px solid #ddd; + border-radius: 8px; + background-color: #f9f9f9; + text-align: center; +} + +.service h2 { + font-size: 1.75rem; + margin-bottom: 1rem; + color: #003366; +} + +.service p { + font-size: 1rem; + margin-bottom: 1rem; +} + +.service ul { + text-align: left; + margin-bottom: 1.5rem; + padding-left: 20px; +} + +.service ul li { + margin-bottom: 0.5rem; +} + +.service .cta-button { + background-color: #007BFF; + color: white; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + text-decoration: none; + font-size: 1rem; + transition: background-color 0.3s ease; +} + +.service .cta-button:hover { + background-color: #0056b3; +} + +/* Call-to-Action Section */ +.call-to-action { + text-align: center; + padding: 2rem; + background-color: #003366; + color: white; + border-radius: 10px; + margin-top: 2rem; +} + +.call-to-action h2 { + font-size: 2rem; + margin-bottom: 1rem; +} + +.call-to-action p { + font-size: 1.25rem; + margin-bottom: 1.5rem; +} + +.call-to-action .cta-button { + background-color: #ffcc00; + color: #003366; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + text-decoration: none; + font-size: 1rem; + transition: background-color 0.3s ease; +} + +.call-to-action .cta-button:hover { + background-color: #e6b800; +} + +/* Footer */ +footer { + background-color: #003366; + color: white; + text-align: center; + padding: 20px; + position: relative; + bottom: 0; + width: 100%; + margin-top: 40px; +} + +/* Responsive Design */ +@media (max-width: 768px) { + header .container { + flex-direction: column; + align-items: center; + } + + header nav ul { + flex-direction: column; + align-items: center; + margin-top: 10px; + } + + header nav ul li { + margin: 10px 0; + } + + .feature-item, + .service { + flex: 1 1 100%; + } + + .hero h1 { + font-size: 2rem; + } + + .hero p { + font-size: 1rem; + } +} \ No newline at end of file diff --git a/musango-app/public/images/logo.png b/musango-app/public/images/logo.png new file mode 100644 index 0000000..4eeeec8 Binary files /dev/null and b/musango-app/public/images/logo.png differ diff --git a/musango-app/public/js/script.js b/musango-app/public/js/script.js new file mode 100644 index 0000000..f329d6e --- /dev/null +++ b/musango-app/public/js/script.js @@ -0,0 +1,96 @@ +document.addEventListener("DOMContentLoaded", function () { + console.log("Musango Express loaded!"); + + // Populate dynamic time options (every 3 hours) + const timeSelect = document.getElementById("time"); + if (timeSelect) { + const times = ["06:00 AM", "09:00 AM", "12:00 PM", "03:00 PM", "06:00 PM", "09:00 PM"]; + times.forEach(time => { + const option = document.createElement("option"); + option.value = time; + option.textContent = time; + timeSelect.appendChild(option); + }); + } + + // Add smooth scrolling for menu links + document.querySelectorAll("nav ul li a").forEach(link => { + link.addEventListener("click", function (e) { + const href = this.getAttribute("href"); + if (href.startsWith("#")) { + e.preventDefault(); + const target = document.querySelector(href); + if (target) { + target.scrollIntoView({ behavior: "smooth" }); + } + } + }); + }); + + // Highlight active menu item based on scroll position + const sections = document.querySelectorAll("section"); + const navLinks = document.querySelectorAll("nav ul li a"); + + function highlightActiveMenu() { + let currentSection = ""; + sections.forEach(section => { + const sectionTop = section.offsetTop; + const sectionHeight = section.clientHeight; + if (window.scrollY >= sectionTop - sectionHeight / 3) { + currentSection = section.getAttribute("id"); + } + }); + + navLinks.forEach(link => { + link.classList.remove("active"); + if (link.getAttribute("href") === `#${currentSection}`) { + link.classList.add("active"); + } + }); + } + + window.addEventListener("scroll", highlightActiveMenu); + highlightActiveMenu(); // Call once on page load + + // Optional: Add a scroll-to-top button + const scrollToTopButton = document.createElement("button"); + scrollToTopButton.textContent = "↑"; + scrollToTopButton.classList.add("scroll-to-top"); + document.body.appendChild(scrollToTopButton); + + scrollToTopButton.addEventListener("click", () => { + window.scrollTo({ top: 0, behavior: "smooth" }); + }); + + window.addEventListener("scroll", () => { + if (window.scrollY > 500) { + scrollToTopButton.style.display = "block"; + } else { + scrollToTopButton.style.display = "none"; + } + }); + + // Optional: Add form validation for the contact form + const contactForm = document.getElementById("contact-form"); + if (contactForm) { + contactForm.addEventListener("submit", function (e) { + const name = document.getElementById("name").value.trim(); + const email = document.getElementById("email").value.trim(); + const message = document.getElementById("message").value.trim(); + + if (!name || !email || !message) { + e.preventDefault(); + alert("Please fill out all fields before submitting."); + } else if (!validateEmail(email)) { + e.preventDefault(); + alert("Please enter a valid email address."); + } + }); + } + + // Helper function to validate email + function validateEmail(email) { + const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return regex.test(email); + } +}); \ No newline at end of file diff --git a/musango-app/public/receipts/REC-124586.pdf b/musango-app/public/receipts/REC-124586.pdf new file mode 100644 index 0000000..15f7d5e Binary files /dev/null and b/musango-app/public/receipts/REC-124586.pdf differ diff --git a/musango-app/public/receipts/REC-191241.pdf b/musango-app/public/receipts/REC-191241.pdf new file mode 100644 index 0000000..b20dbed Binary files /dev/null and b/musango-app/public/receipts/REC-191241.pdf differ diff --git a/musango-app/public/receipts/REC-227759.pdf b/musango-app/public/receipts/REC-227759.pdf new file mode 100644 index 0000000..f970795 Binary files /dev/null and b/musango-app/public/receipts/REC-227759.pdf differ diff --git a/musango-app/public/receipts/REC-288725.pdf b/musango-app/public/receipts/REC-288725.pdf new file mode 100644 index 0000000..4a6cc67 Binary files /dev/null and b/musango-app/public/receipts/REC-288725.pdf differ diff --git a/musango-app/public/receipts/REC-313952.pdf b/musango-app/public/receipts/REC-313952.pdf new file mode 100644 index 0000000..95f2646 Binary files /dev/null and b/musango-app/public/receipts/REC-313952.pdf differ diff --git a/musango-app/public/receipts/REC-317057.pdf b/musango-app/public/receipts/REC-317057.pdf new file mode 100644 index 0000000..0982189 Binary files /dev/null and b/musango-app/public/receipts/REC-317057.pdf differ diff --git a/musango-app/public/receipts/REC-382336.pdf b/musango-app/public/receipts/REC-382336.pdf new file mode 100644 index 0000000..c50a502 Binary files /dev/null and b/musango-app/public/receipts/REC-382336.pdf differ diff --git a/musango-app/public/receipts/REC-444473.pdf b/musango-app/public/receipts/REC-444473.pdf new file mode 100644 index 0000000..0df4445 Binary files /dev/null and b/musango-app/public/receipts/REC-444473.pdf differ diff --git a/musango-app/public/receipts/REC-463189.pdf b/musango-app/public/receipts/REC-463189.pdf new file mode 100644 index 0000000..c2ceee9 Binary files /dev/null and b/musango-app/public/receipts/REC-463189.pdf differ diff --git a/musango-app/public/receipts/REC-497550.pdf b/musango-app/public/receipts/REC-497550.pdf new file mode 100644 index 0000000..eaf445e Binary files /dev/null and b/musango-app/public/receipts/REC-497550.pdf differ diff --git a/musango-app/public/receipts/REC-602510.pdf b/musango-app/public/receipts/REC-602510.pdf new file mode 100644 index 0000000..03509ed Binary files /dev/null and b/musango-app/public/receipts/REC-602510.pdf differ diff --git a/musango-app/public/receipts/REC-635137.pdf b/musango-app/public/receipts/REC-635137.pdf new file mode 100644 index 0000000..49a73d4 Binary files /dev/null and b/musango-app/public/receipts/REC-635137.pdf differ diff --git a/musango-app/public/receipts/REC-636587.pdf b/musango-app/public/receipts/REC-636587.pdf new file mode 100644 index 0000000..791074b Binary files /dev/null and b/musango-app/public/receipts/REC-636587.pdf differ diff --git a/musango-app/public/receipts/REC-642058.pdf b/musango-app/public/receipts/REC-642058.pdf new file mode 100644 index 0000000..f5a13ac Binary files /dev/null and b/musango-app/public/receipts/REC-642058.pdf differ diff --git a/musango-app/public/receipts/REC-757901.pdf b/musango-app/public/receipts/REC-757901.pdf new file mode 100644 index 0000000..316fc9b Binary files /dev/null and b/musango-app/public/receipts/REC-757901.pdf differ diff --git a/musango-app/public/receipts/REC-780459.pdf b/musango-app/public/receipts/REC-780459.pdf new file mode 100644 index 0000000..917fbcb Binary files /dev/null and b/musango-app/public/receipts/REC-780459.pdf differ diff --git a/musango-app/public/receipts/REC-859031.pdf b/musango-app/public/receipts/REC-859031.pdf new file mode 100644 index 0000000..852abef Binary files /dev/null and b/musango-app/public/receipts/REC-859031.pdf differ diff --git a/musango-app/public/receipts/REC-909476.pdf b/musango-app/public/receipts/REC-909476.pdf new file mode 100644 index 0000000..66cffe9 Binary files /dev/null and b/musango-app/public/receipts/REC-909476.pdf differ diff --git a/musango-app/public/receipts/REC-911848.pdf b/musango-app/public/receipts/REC-911848.pdf new file mode 100644 index 0000000..4ba8401 Binary files /dev/null and b/musango-app/public/receipts/REC-911848.pdf differ diff --git a/musango-app/routes/booking.js b/musango-app/routes/booking.js new file mode 100644 index 0000000..b63be0f --- /dev/null +++ b/musango-app/routes/booking.js @@ -0,0 +1,118 @@ +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 + }); + } + + if (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'); + if (!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) + if (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; diff --git a/musango-app/routes/index.js b/musango-app/routes/index.js new file mode 100644 index 0000000..afcde50 --- /dev/null +++ b/musango-app/routes/index.js @@ -0,0 +1,34 @@ +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; diff --git a/musango-app/test-db.js b/musango-app/test-db.js new file mode 100644 index 0000000..71d304d --- /dev/null +++ b/musango-app/test-db.js @@ -0,0 +1,11 @@ +// test-db.js +const mongoose = require('mongoose'); + +mongoose.connect('mongodb://localhost:27017/musango-express') + .then(() => { + console.log('βœ… MongoDB connected!'); + mongoose.disconnect(); + }) + .catch(err => { + console.error('❌ MongoDB connection failed:', err); + }); diff --git a/musango-app/test/app.test.js b/musango-app/test/app.test.js new file mode 100644 index 0000000..d1809dc --- /dev/null +++ b/musango-app/test/app.test.js @@ -0,0 +1,42 @@ +const request = require('supertest'); +const mongoose = require('mongoose'); +const { createServer } = require('../app'); + +let app, server; + +beforeAll(async () => { + await mongoose.connect(process.env.MONGO_URI); + app = createServer(); + server = app.listen(0); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await server.close(); +}); + +describe('Musango Express App', () => { + it('should return 200 for /health', async () => { + const response = await request(app).get('/health'); + expect(response.statusCode).toBe(200); + expect(response.body.status).toBe('OK'); + }); + + it('should render home page', async () => { + const response = await request(app).get('/'); + expect(response.statusCode).toBe(200); + expect(response.text).toContain('Welcome to Musango Express'); + }); + + it('should render services page', async () => { + const response = await request(app).get('/services'); + expect(response.statusCode).toBe(200); + expect(response.text).toContain('Transportation'); + }); + + it('should render about page', async () => { + const response = await request(app).get('/about'); + expect(response.statusCode).toBe(200); + expect(response.text).toContain('About Us'); + }); +}); diff --git a/musango-app/test/booking.test.js b/musango-app/test/booking.test.js new file mode 100644 index 0000000..c6914e8 --- /dev/null +++ b/musango-app/test/booking.test.js @@ -0,0 +1,131 @@ +const request = require('supertest'); +const mongoose = require('mongoose'); +const { createServer } = require('../app'); +const fs = require('fs'); +const path = require('path'); + +let app, server; + +beforeAll(async () => { + await mongoose.connect(process.env.MONGO_URI, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + app = createServer(); + server = app.listen(0); +}); + +afterAll(async () => { + await mongoose.disconnect(); + if (server) server.close(); + + // Optional: Clean up test-generated receipts + const receiptsDir = path.join(__dirname, '..', 'public', 'receipts'); + fs.readdirSync(receiptsDir) + .filter(file => file.startsWith('REC-')) + .forEach(file => fs.unlinkSync(path.join(receiptsDir, file))); +}); + +describe('Booking Routes', () => { + test('Booking page returns 400 without destination', async () => { + const res = await request(app).get('/booking'); + expect(res.statusCode).toBe(400); + expect(res.text).toContain('Destination is required'); + }); + + test('Booking page renders form with destination', async () => { + const res = await request(app).get('/booking?destination=Yaounde'); + expect(res.statusCode).toBe(200); + expect(res.text).toContain('Book Your Ticket to Yaounde'); + expect(res.text).toContain('name="destination" value="Yaounde"'); + }); + + test('Rejects booking with missing fields', async () => { + const res = await request(app).post('/book').send({ + name: 'Test User' + }); + expect(res.statusCode).toBe(400); + expect(res.text).toContain('All fields are required'); + }); + + test('Rejects booking if age is not a number', async () => { + const res = await request(app).post('/book').send({ + destination: 'Douala', + name: 'Test User', + age: 'abc', + contact: '691234567', + email: 'valid@gmail.com', + date: '2025-04-15', + time: '06:00 AM', + busSerial: 'DOU-001' + }); + expect(res.statusCode).toBe(400); + expect(res.text).toContain('Age must be a number'); + }); + + test('Rejects booking if email is missing', async () => { + const res = await request(app).post('/book').send({ + destination: 'Buea', + name: 'No Email User', + age: 30, + contact: '6901234567', + date: '2025-04-21', + time: '12:00 PM', + busSerial: 'BUE-001' + }); + expect(res.statusCode).toBe(400); + expect(res.text).toContain('All fields are required'); + }); + + test('Rejects booking if email is not Gmail', async () => { + const res = await request(app).post('/book').send({ + destination: 'Limbe', + name: 'Wrong Email', + age: 32, + contact: '6901234567', + email: 'wrong@outlook.com', + date: '2025-04-21', + time: '03:00 PM', + busSerial: 'LIM-002' + }); + expect(res.statusCode).toBe(400); + expect(res.text).toContain('Only Gmail addresses are accepted'); + }); + + test('Creates a booking and saves PDF', async () => { + const email = `pdfbooker${Date.now()}@gmail.com`; + const res = await request(app).post('/book').send({ + destination: 'Bamenda', + name: 'PDF Booker', + age: 29, + contact: '6911122334', + email, + date: '2025-04-22', + time: '06:00 AM', + busSerial: 'BAM-001' + }); + + expect(res.statusCode).toBe(200); + expect(res.text).toContain('Booking Successful'); + expect(res.text).toContain('PDF Booker'); + + // Extract receipt number from response HTML + const match = res.text.match(/Receipt Number:<\/strong>\s*(REC-\d{6})/); + expect(match).not.toBeNull(); + + const receiptNumber = match[1]; + const receiptPath = path.join(__dirname, '..', 'public', 'receipts', `${receiptNumber}.pdf`); + + // Wait up to 2 seconds for PDF to be saved + let found = false; + for (let i = 0; i < 10; i++) { + if (fs.existsSync(receiptPath)) { + found = true; + break; + } + await new Promise(res => setTimeout(res, 200)); + } + + expect(found).toBe(true); + }); +}); diff --git a/musango-app/test/setup.js b/musango-app/test/setup.js new file mode 100644 index 0000000..923786a --- /dev/null +++ b/musango-app/test/setup.js @@ -0,0 +1 @@ +process.env.MONGO_URI = "mongodb://localhost:27017/musango_test"; diff --git a/musango-app/utils/mailer.js b/musango-app/utils/mailer.js new file mode 100644 index 0000000..7fd4be1 --- /dev/null +++ b/musango-app/utils/mailer.js @@ -0,0 +1,31 @@ +// 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" ', + 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; diff --git a/musango-app/utils/pdfGenerator.js b/musango-app/utils/pdfGenerator.js new file mode 100644 index 0000000..437a560 --- /dev/null +++ b/musango-app/utils/pdfGenerator.js @@ -0,0 +1,39 @@ +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 with --no-sandbox flag to avoid sandbox issues + const browser = await puppeteer.launch({ + headless: true, // Use headless mode for production + args: ['--no-sandbox', '--disable-setuid-sandbox'], // Add these arguments to bypass sandboxing + }); + + 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 }; diff --git a/musango-app/views/about.ejs b/musango-app/views/about.ejs new file mode 100644 index 0000000..448e365 --- /dev/null +++ b/musango-app/views/about.ejs @@ -0,0 +1,79 @@ + + + + + + About Us - Musango Express + + + +
+
+ + +
+
+ +
+

About Us

+

Musango Express is a leading transport and mailing company in Cameroon, providing reliable and efficient services to our customers since 1999. With over two decades of experience, we have built a reputation for excellence, safety, and customer satisfaction.

+ +

Our Mission

+

Our mission is to make travel and delivery seamless, affordable, and enjoyable for everyone. We strive to connect people, businesses, and communities by offering top-notch transport and mailing solutions tailored to meet the needs of our diverse clientele.

+ +

Our Vision

+

We envision a future where transportation and logistics are no longer a barrier to growth and connectivity. By leveraging cutting-edge technology and a customer-first approach, we aim to become the most trusted and innovative transport and mailing service provider in the region.

+ +

Our Values

+
    +
  • Reliability: We are committed to delivering on our promises, ensuring that our services are dependable and consistent.
  • +
  • Safety: The safety of our passengers, staff, and parcels is our top priority. We adhere to the highest safety standards in all our operations.
  • +
  • Customer Focus: We listen to our customers and continuously improve our services to meet their evolving needs.
  • +
  • Innovation: We embrace innovation to enhance efficiency, reduce costs, and provide a superior customer experience.
  • +
  • Integrity: We conduct our business with honesty, transparency, and respect for all stakeholders.
  • +
+ +

Our History

+

Founded in 1999 by Mr. John Musango, Musango Express started as a small local transport service with just two vehicles. Over the years, we have grown exponentially, expanding our fleet, services, and reach across Cameroon and beyond. Today, we operate a modern fleet of buses, trucks, and delivery vans, serving thousands of customers daily.

+ +

Our Team

+

At Musango Express, we believe that our people are our greatest asset. Our team consists of highly skilled and dedicated professionals, including drivers, logistics experts, customer service representatives, and management staff. Together, we work tirelessly to ensure that every journey and delivery is a success.

+ +

Our Services

+

We offer a wide range of services designed to meet the needs of individuals, businesses, and organizations. These include:

+
    +
  • Passenger Transport: Comfortable and affordable bus services to major cities and towns across Cameroon.
  • +
  • Parcel Delivery: Fast and reliable delivery of packages, documents, and goods to any destination.
  • +
  • Logistics Solutions: Customized logistics services for businesses, including warehousing, distribution, and supply chain management.
  • +
  • Corporate Services: Specialized transport and mailing solutions for corporate clients, including scheduled deliveries and executive travel.
  • +
+ +

Why Choose Musango Express?

+

Choosing Musango Express means choosing a partner you can trust. Here are some reasons why our customers prefer us:

+
    +
  • Extensive Network: We operate in over 50 cities and towns, ensuring that we can serve you wherever you are.
  • +
  • Affordable Rates: We offer competitive pricing without compromising on quality or safety.
  • +
  • Customer Support: Our friendly and knowledgeable customer support team is available 24/7 to assist you with any inquiries or issues.
  • +
  • Eco-Friendly Practices: We are committed to reducing our environmental impact by adopting sustainable practices and using fuel-efficient vehicles.
  • +
+ +

Community Involvement

+

At Musango Express, we believe in giving back to the communities we serve. We actively participate in various social responsibility initiatives, including education programs, environmental conservation projects, and support for local businesses. Through these efforts, we aim to make a positive impact on society and contribute to the development of our nation.

+ +

Contact Us

+

We would love to hear from you! Whether you have a question, feedback, or need assistance, our team is here to help. Visit our Contacts page for more information on how to reach us.

+
+ +
+

© 2023 Musango Express. All rights reserved.

+
+ + \ No newline at end of file diff --git a/musango-app/views/booking-form.ejs b/musango-app/views/booking-form.ejs new file mode 100644 index 0000000..d6964d7 --- /dev/null +++ b/musango-app/views/booking-form.ejs @@ -0,0 +1,133 @@ + + + + + + Book Ticket - Musango Express + + + + +
+
+ + +
+
+ +
+

Book Your Ticket to <%= destination %>

+ + <% if (error) { %> +
+

<%= error %>

+
+ <% } %> + +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+ +
+

© <%= new Date().getFullYear() %> Musango Express. All rights reserved.

+
+ + diff --git a/musango-app/views/booking-success.ejs b/musango-app/views/booking-success.ejs new file mode 100644 index 0000000..580a533 --- /dev/null +++ b/musango-app/views/booking-success.ejs @@ -0,0 +1,112 @@ + + + + + + Booking Successful - Musango Express + + + + +
+
+ + +
+
+ +
+
+

Booking Successful!

+

Thank you for booking with Musango Express. Here are your ticket details:

+
+

Receipt Number: <%= booking.receiptNumber %>

+

Destination: <%= booking.destination %>

+

Name: <%= booking.name %>

+

Age: <%= booking.age %>

+

Contact: <%= booking.contact %>

+

Date: + <%= (booking.date instanceof Date && !isNaN(booking.date)) + ? booking.date.toLocaleDateString() + : new Date(booking.date).toLocaleDateString() %> +

+

Time: <%= booking.time %>

+

Bus Serial: <%= booking.busSerial %>

+

Price: 10,000 XAF

+
+

We wish you a safe and enjoyable journey!

+ +
+
+ +
+

© 2023 Musango Express. All rights reserved.

+
+ + diff --git a/musango-app/views/booking.ejs b/musango-app/views/booking.ejs new file mode 100644 index 0000000..72de5ec --- /dev/null +++ b/musango-app/views/booking.ejs @@ -0,0 +1,128 @@ + + + + + + Booking Confirmation - Hill-Top Consultancy + + + + + +
+

Hill-Top Consultancy - Musango Express

+
+ +
+ + +

Booking Confirmation

+

Thank you for choosing Hill-Top Consultancy's Musango Express service. Below are your booking details:

+ +
+

Receipt Number: <%= booking.receiptNumber %>

+

Passenger Name: <%= booking.name %>

+

Age: <%= booking.age %>

+

Contact: <%= booking.contact %>

+

Email: <%= booking.email %>

+

Trip From: <%= booking.from %>

+

Destination: <%= booking.destination %>

+

Date: <%= booking.date %>

+

Time: <%= booking.time %>

+

Bus Serial: <%= booking.busSerial %>

+
+ +
+

Base Price: 10,000 XAF

+

Tax (24%): 2,400 XAF

+

Total: 12,400 XAF

+
+ + Return to Homepage +
+ +
+

© <%= new Date().getFullYear() %> Hill-Top Consultancy - All Rights Reserved.

+

Consulting services in IT & Management | ENK Sole Proprietorship

+

Fully Responsible: Forchu Sani Prince Chafah

+
+ + + diff --git a/musango-app/views/contacts.ejs b/musango-app/views/contacts.ejs new file mode 100644 index 0000000..b9adcfe --- /dev/null +++ b/musango-app/views/contacts.ejs @@ -0,0 +1,79 @@ + + + + + + Contacts - Musango Express + + + +
+
+ + +
+
+ +
+

Contact Us

+

We’d love to hear from you! Whether you have a question, feedback, or need assistance, our team is here to help. Reach out to us via the contact details below or fill out the form, and we’ll get back to you as soon as possible.

+ +
+

Our Contact Details

+

Address: Hoje Taastrup Denmark

+

Email: info@htconsult.dk

+

Phone: +45 71573047

+
+ +
+

Send Us a Message

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+

© 2023 Musango Express. All rights reserved.

+
+ + \ No newline at end of file diff --git a/musango-app/views/destinations.ejs b/musango-app/views/destinations.ejs new file mode 100644 index 0000000..c39cb72 --- /dev/null +++ b/musango-app/views/destinations.ejs @@ -0,0 +1,83 @@ + + + + + + Destinations - Musango Express + + + + +
+
+ + +
+
+ +
+

Destinations

+ + + + + + + + + <% regions.forEach(region => { %> + + + + + <% }) %> + +
RegionAction
<%= region %> + Book Now +
+
+ +
+

© 2023 Musango Express. All rights reserved.

+
+ + \ No newline at end of file diff --git a/musango-app/views/error.ejs b/musango-app/views/error.ejs new file mode 100644 index 0000000..10e105e --- /dev/null +++ b/musango-app/views/error.ejs @@ -0,0 +1,36 @@ + + + + Error - Musango Express + + + + +
+

Something went wrong πŸ˜₯

+

<%= message || "We couldn't process your request at the moment." %>

+ Back to Home +
+ + diff --git a/musango-app/views/index.ejs b/musango-app/views/index.ejs new file mode 100644 index 0000000..6280234 --- /dev/null +++ b/musango-app/views/index.ejs @@ -0,0 +1,140 @@ + + + + + + <%= title %> - Musango Express + + + + +
+
+ + +
+
+ +
+
+

Welcome to Musango Express by Hill-Top Consultancy

+

+ Your reliable partner for safe, affordable, and efficient transportation and mailing services across Cameroon. + We connect people and businesses with speed, comfort, and reliability. Whether you're booking a trip or sending a parcel, + trust Musango Express for dependable service every time. + Hosted at Hoje Taastrup - Denmark +

+ Book Your Trip +
+ +
+
+

Transportation

+

Reliable and comfortable transportation services across all regions of Cameroon. Modern fleet, experienced drivers, and safe travel guaranteed.

+
+
+

Mailing

+

Fast, secure, and affordable mailing services to all major cities and towns. Send and receive parcels with confidence.

+
+
+

Online Reservations

+

Book your tickets online with ease using our user-friendly platform. Available 24/7 to serve you better.

+
+
+
+ +
+

© <%= new Date().getFullYear() %> Musango Express. All rights reserved.

+
+ + diff --git a/musango-app/views/receipt.ejs b/musango-app/views/receipt.ejs new file mode 100644 index 0000000..517422d --- /dev/null +++ b/musango-app/views/receipt.ejs @@ -0,0 +1,113 @@ + + + + + Receipt - <%= booking.receiptNumber %> + + + + +
+ +
+ HILL-TOP CONSULTANCY
+ CVR: 44814544
+ Sylen 3, 2. 0525
+ 2630 Taastrup, HΓΈje-Taastrup
+ Denmark
+ Email: info@htconsult.dk
+ Phone: 71 57 30 47
+ www.htconsult.dk +
+
+ +

Booking Receipt

+ +
+
Client Information
+

Name: <%= booking.name %>

+

Email: <%= booking.email %>

+

Contact: <%= booking.contact %>

+

Receipt No.: <%= booking.receiptNumber %>

+

Date of Issue: <%= new Date().toLocaleDateString() %>

+
+ +
+
Booking Details
+

From: <%= booking.from || 'N/A' %>

+

To: <%= booking.destination %>

+

Travel Date: <%= booking.date %>

+

Time: <%= booking.time %>

+

Bus Serial: <%= booking.busSerial %>

+
+ +
+

Base Price: 10,000 XAF

+

Tax (24%): 2,400 XAF

+

Total Paid: 12,400 XAF

+
+ +
+ Thank you for booking with Hill-Top Consultancy. Safe travels!
+ This receipt was generated electronically and is valid without a signature. +
+ + + diff --git a/musango-app/views/services.ejs b/musango-app/views/services.ejs new file mode 100644 index 0000000..afd0f02 --- /dev/null +++ b/musango-app/views/services.ejs @@ -0,0 +1,78 @@ + + + + + + Services - Musango Express + + + +
+
+ + +
+
+ +
+

Our Services

+

At Musango Express, we are committed to providing top-notch transportation and mailing services tailored to meet your needs. Explore our range of services below:

+ +
+
+

Transportation

+

We offer reliable and comfortable transportation services across all regions of Cameroon. Our fleet of modern buses ensures a safe and enjoyable journey.

+
    +
  • Intercity and intracity travel options.
  • +
  • Comfortable seating with air conditioning.
  • +
  • Regular and express routes available.
  • +
  • Affordable pricing for all budgets.
  • +
+ View Destinations +
+ +
+

Mailing

+

Our mailing services are fast, secure, and affordable. We deliver packages to all major cities and towns in Cameroon.

+
    +
  • Same-day and next-day delivery options.
  • +
  • Parcel tracking for real-time updates.
  • +
  • Secure handling of sensitive packages.
  • +
  • Competitive rates for individuals and businesses.
  • +
+ Request a Quote +
+ +
+

Online Reservations

+

Book your tickets online with ease. Our user-friendly platform allows you to reserve your seat in just a few clicks.

+
    +
  • 24/7 online booking availability.
  • +
  • Instant confirmation and e-tickets.
  • +
  • Flexible payment options.
  • +
  • Manage bookings and cancellations online.
  • +
+ Book Now +
+
+ +
+

Need Help Choosing a Service?

+

Our team is here to assist you. Contact us today to learn more about our services or to get personalized recommendations.

+ Contact Us +
+
+ + + + \ No newline at end of file