Compare commits

...

11 Commits

Author SHA1 Message Date
joelthomastrenser 714067f309 Merged PR 1202: Vehicle Service System v1.2.0.0
## New Features

### Shared Memory DataStore

* Replaced the existing in-memory DataStore with a shared-memory implementation using Windows File Mapping.
* Added support for sharing application data between multiple running instances.
* Created separate mappings for users, notifications, services, bookings, invoices, payments, and other entities.

### Binary Data Storage

* Added binary serialization for all major entities.
* Introduced record tracking to handle new, modified, and deleted records.
* Added automatic growth and shrinking of mappings when required.

## Refactoring

### DataStore Changes

* Refactored DataStore to use shared memory instead of local process memory.
* Added generic helper functions for loading and saving records.
* Updated application startup and shutdown to initialize and clean up shared-memory resources.

### Synchronization

* Added a global mutex to synchronize datastore operations across processes.
* Added remapping logic to detect mapping size changes made by other instances.

## Bug Fixes

* Fixed issues with data not being shared correctly between multiple application instances.
* Improved reliability of record updates and deletions.
* Fixed stale mapping issues when mappings were resized by another process.

Related work items: #1925, #1926, #1927, #1928, #1929, #1930, #1949, #1950, #1951, #1952, #1953, #1954, #1955, #1956, #1957, #1958, #1959, #1960, #2061, #2074, #2075, #2076, #2077, #2078, #2079, #2080, #2081, #2082, #2100, #2105, #2114, #2115, #2116
2026-06-19 16:51:22 +05:30
Avinash Rajesh 05b9fc7962 Merged PR 1200: Fix Customer Allowed to Create Service Bookings While Having Unpaid Invoices
Changes:
- Added verifyAllPaymentsCompleted() helper in MenuHelper.h to check if the authenticated customer has completed all invoices.
- Integrated payment verification into CustomerMenu::selectService() to block new service bookings when pending payments exist.
- Integrated payment verification into CustomerMenu::selectComboPackage() to block new combo package bookings when invoices are incomplete.
- Implemented iteration over customer invoices to ensure only users with all payments marked as COMPLETED can proceed with new bookings.
- Added user-facing messages to inform customers when bookings are denied due to outstanding payments.

Related work items: #2116
2026-06-18 19:55:12 +05:30
joelthomastrenser 956ef58c79 Implement review fixes 2026-06-18 19:54:12 +05:30
Jissin Mathew 32f1fa33ce Merged PR 1199: Fix Multiple Notifications Contain Formatting Issues, Vague Messaging, and Duplicate Entries
Changes:

- Updated ServiceManagementService::createJobCard() to prevent duplicate
  "Technician assigned" notifications when multiple job cards exist for
  the same booking.
- Refined job card creation notification to include Job Card ID, Service
  ID, and Booking ID for clearer technician communication.
- Corrected service booking cancellation notification formatting by
  standardizing capitalization and improving message clarity.
- Improved service booking completion notification to explicitly mention
  the booking ID and confirm invoice generation.
- Ensured consistent notification titles and messages across the
  workflow for better user understanding.

Fixes

#2114

Related work items: #2114
2026-06-18 19:44:35 +05:30
Jissin Mathew 684d6d3860 Implement review fixes 2026-06-18 19:43:41 +05:30
joelthomastrenser 976547e7d1 Merged PR 1201: Fix invoice display issues and improve combo package handling
**Changes**:

- Removed inventory quantity from invoice item display.
- Added null checks when displaying combo packages.
- Added null checks when selecting combo packages.
- Replaced unnecessary object copies with references.
- Zero-initialized SerializedObserver.
- Initialized event handle array before use.

Fixes
#2115

Related work items: #2115
2026-06-18 19:37:31 +05:30
joelthomastrenser 4fa143946e Fix combo package null handling and cleanup initialization
- Added null checks in combo package display/selection flows
- Replaced unnecessary copies with references
- Zero-initialized SerializedObserver
- Initialized event handle array
2026-06-18 19:31:18 +05:30
Avinash Rajesh bdb8431773 Customer Allowed to Create Service Bookings While Having Unpaid Invoices
- Added verifyAllPaymentsCompleted() helper in MenuHelper.h to check if the authenticated customer has completed all invoices.

- Integrated payment verification into CustomerMenu::selectService() to block new service bookings when pending payments exist.

- Integrated payment verification into CustomerMenu::selectComboPackage() to block new combo package bookings when invoices are incomplete.

- Implemented iteration over customer invoices to ensure only users with all payments marked as COMPLETED can proceed with new bookings.

- Added user-facing messages to inform customers when bookings are denied due to outstanding payments.

Fixes #2116
2026-06-18 19:26:45 +05:30
Jissin Mathew 2eaa719aca Fix Multiple Notifications Contain Formatting Issues, Vague Messaging, and Duplicate Entries
Changes:

- Updated ServiceManagementService::createJobCard() to prevent duplicate
  "Technician assigned" notifications when multiple job cards exist for
  the same booking.
- Refined job card creation notification to include Job Card ID, Service
  ID, and Booking ID for clearer technician communication.
- Corrected service booking cancellation notification formatting by
  standardizing capitalization and improving message clarity.
- Improved service booking completion notification to explicitly mention
  the booking ID and confirm invoice generation.
- Ensured consistent notification titles and messages across the
  workflow for better user understanding.

Fixes #2114
2026-06-18 19:21:59 +05:30
joelthomastrenser 931913fa30 Fix: remove inventory quantity from invoice display
- Removed Quantity column from invoice item listing
- Removed inventory stock quantity values from invoice output
- Display only item name and price in generated invoices

Fixes #2115
2026-06-18 19:20:02 +05:30
joelthomastrenser 8a3ec278ce Merged PR 1159: Vehicle Service System v1.1.0.0
**New Features**

**Payment Confirmation Workflow**
- Added support for payment confirmation by administrators.

- Introduced a new PAID payment status.

- Implemented invoice confirmation flow from PAID → COMPLETED.

- Added invoice filtering based on payment status.

- Added admin menu option to confirm customer payments.

**Technician Job Status Workflow**
- Enhanced technician job management with multi-stage status updates.

- Added support for job status transitions:

- STARTED → IN_PROGRESS

- IN_PROGRESS → COMPLETED

- Added invoice generation and customer notification upon booking completion.

- Improved job visibility by displaying current job status.

**UI Improvements**
- Improved formatting and wording in the Technician Job Status workflow.

- Standardized status labels using "In Progress".

- Improved prompts, headings, and job selection messages.

- Enhanced readability of job listings.

**Bug Fixes**
- Fixed duplicate customer notifications when an assigned technician is removed.

- Prevented creation of duplicate usernames across all user states.

- Fixed authentication conflicts caused by reuse of deleted/disabled usernames.

- Improved booking cancellation handling for customer and technician removal scenarios.

- Updated cancellation logic to correctly handle bookings in IN_PROGRESS state.

Related work items: #1797, #1798, #1807, #1808, #1809
2026-06-01 18:57:46 +05:30
5 changed files with 108 additions and 38 deletions
@@ -358,7 +358,7 @@ util::Map<std::string, TrackedRecord<ServiceBooking>>& DataStore::getServiceBook
{ {
throw std::runtime_error("Invalid service index."); throw std::runtime_error("Invalid service index.");
} }
auto currentService = services.getValueAt(serviceIndex); auto& currentService = services.getValueAt(serviceIndex);
servicesInBooking[currentServiceId] = currentService.data; servicesInBooking[currentServiceId] = currentService.data;
} }
serviceBooking->setServices(servicesInBooking); serviceBooking->setServices(servicesInBooking);
@@ -369,7 +369,7 @@ util::Map<std::string, TrackedRecord<ServiceBooking>>& DataStore::getServiceBook
{ {
throw std::runtime_error("Invalid user index."); throw std::runtime_error("Invalid user index.");
} }
auto customer = users.getValueAt(userIndex); auto& customer = users.getValueAt(userIndex);
serviceBooking->setCustomer(customer.data); serviceBooking->setCustomer(customer.data);
} }
if (!serviceBooking->getAssignedTechnicianId().empty()) if (!serviceBooking->getAssignedTechnicianId().empty())
@@ -379,7 +379,7 @@ util::Map<std::string, TrackedRecord<ServiceBooking>>& DataStore::getServiceBook
{ {
throw std::runtime_error("Invalid technician index."); throw std::runtime_error("Invalid technician index.");
} }
auto technician = users.getValueAt(technicianIndex); auto& technician = users.getValueAt(technicianIndex);
serviceBooking->setAssignedTechnician(technician.data); serviceBooking->setAssignedTechnician(technician.data);
} }
} }
@@ -423,7 +423,7 @@ util::Map<std::string, TrackedRecord<JobCard>>& DataStore::getJobCards()
{ {
throw std::runtime_error("Invalid service ID: " + serviceId); throw std::runtime_error("Invalid service ID: " + serviceId);
} }
auto trackedService = services.getValueAt(serviceIndex); auto& trackedService = services.getValueAt(serviceIndex);
jobCard->setService(trackedService.data); jobCard->setService(trackedService.data);
const std::string& technicianId = jobCard->getTechnicianId(); const std::string& technicianId = jobCard->getTechnicianId();
if (!technicianId.empty()) if (!technicianId.empty())
@@ -433,7 +433,7 @@ util::Map<std::string, TrackedRecord<JobCard>>& DataStore::getJobCards()
{ {
throw std::runtime_error("Invalid technician ID: " + technicianId); throw std::runtime_error("Invalid technician ID: " + technicianId);
} }
auto trackedTechnician = users.getValueAt(technicianIndex); auto& trackedTechnician = users.getValueAt(technicianIndex);
jobCard->setTechnician(trackedTechnician.data); jobCard->setTechnician(trackedTechnician.data);
} }
} }
@@ -691,7 +691,7 @@ void DataStore::saveObservers(MappingInfo& mapping, util::Map<std::string, User*
SharedMemory::setRecordCount(mapping, observerCount); SharedMemory::setRecordCount(mapping, observerCount);
for (size_t index = 0; index < observerCount; index++) for (size_t index = 0; index < observerCount; index++)
{ {
SerializedObserver serializedObserver; SerializedObserver serializedObserver{};
User* user = observers.getValueAt(static_cast<int>(index)); User* user = observers.getValueAt(static_cast<int>(index));
strcpy_s(serializedObserver.id, sizeof(serializedObserver.id), user->getId().c_str()); strcpy_s(serializedObserver.id, sizeof(serializedObserver.id), user->getId().c_str());
SerializedObserver* destination = static_cast<SerializedObserver*>(SharedMemory::getRecordAddress(mapping, index)); SerializedObserver* destination = static_cast<SerializedObserver*>(SharedMemory::getRecordAddress(mapping, index));
@@ -629,6 +629,7 @@ void ServiceManagementService::createJobCard(const std::string& bookingID, const
DataStoreLockGuard lock(m_dataStore); DataStoreLockGuard lock(m_dataStore);
UserManagementService m_userManagementService; UserManagementService m_userManagementService;
ServiceBooking* currentBooking = getServiceBooking(bookingID); ServiceBooking* currentBooking = getServiceBooking(bookingID);
std::string title, message;
if (currentBooking == nullptr) if (currentBooking == nullptr)
{ {
throw std::runtime_error("Service Booking not available"); throw std::runtime_error("Service Booking not available");
@@ -684,18 +685,30 @@ void ServiceManagementService::createJobCard(const std::string& bookingID, const
trackedCurrentInventoryItem.state = RecordState::MODIFIED; trackedCurrentInventoryItem.state = RecordState::MODIFIED;
} }
} }
currentBooking->setAssignedTechnician(selectedTechnician); const User* currentAssignedTechnician = currentBooking->getAssignedTechnician();
currentBooking->setAssignedTechnicianId(selectedTechnician->getId()); const std::string& currentAssignedTechnicianId = currentBooking->getAssignedTechnicianId();
if (!currentAssignedTechnician && currentAssignedTechnicianId.empty())
{
currentBooking->setAssignedTechnician(selectedTechnician);
currentBooking->setAssignedTechnicianId(selectedTechnician->getId());
title = "Technician assigned";
message = "A technician has been assigned to your Service Booking with ID " + bookingID;
sendNotification(currentBooking->getCustomer(), title, message);
}
if (currentBooking->getStatus() == util::ServiceJobStatus::PENDING) if (currentBooking->getStatus() == util::ServiceJobStatus::PENDING)
{ {
currentBooking->setStatus(util::ServiceJobStatus::STARTED); currentBooking->setStatus(util::ServiceJobStatus::STARTED);
} }
currentTrackedServiceBooking.state = RecordState::MODIFIED; currentTrackedServiceBooking.state = RecordState::MODIFIED;
std::string title = "Job card created";
std::string message = "Job card created for the service and you are assigned for that.";
JobCard* jobCard = Factory::getObject<JobCard>(bookingID, currentBooking, currentService, serviceID, technicianID, selectedTechnician, util::Timestamp(), util::ServiceJobStatus::STARTED, util::Timestamp()); JobCard* jobCard = Factory::getObject<JobCard>(bookingID, currentBooking, currentService, serviceID, technicianID, selectedTechnician, util::Timestamp(), util::ServiceJobStatus::STARTED, util::Timestamp());
if (jobCard) if (jobCard)
{ {
title = "Job Card Assigned";
message = "A new Job Card (ID: " + jobCard->getId() +
") has been created for Service " + serviceID +
" in Booking " + bookingID +
". You have been assigned to this job.";
currentTrackedJobCards.insert(jobCard->getId(), util::createNewRecord(jobCard)); currentTrackedJobCards.insert(jobCard->getId(), util::createNewRecord(jobCard));
sendNotification(selectedTechnician, title, message); sendNotification(selectedTechnician, title, message);
} }
@@ -703,9 +716,6 @@ void ServiceManagementService::createJobCard(const std::string& bookingID, const
{ {
throw std::runtime_error("Failed to create job card."); throw std::runtime_error("Failed to create job card.");
} }
title = "Technician assigned";
message = "A technician has been assigned to your Service Booking with ID " + bookingID;
sendNotification(currentBooking->getCustomer(), title, message);
m_dataStore.saveJobCards(); m_dataStore.saveJobCards();
m_dataStore.saveServiceBookings(); m_dataStore.saveServiceBookings();
m_dataStore.saveInventoryItems(); m_dataStore.saveInventoryItems();
@@ -852,8 +862,8 @@ void ServiceManagementService::removeServiceBooking(const std::string& bookingID
{ {
if (currentServiceBooking->getStatus() == util::ServiceJobStatus::PENDING) if (currentServiceBooking->getStatus() == util::ServiceJobStatus::PENDING)
{ {
const std::string title = "Service Booking cancelled."; const std::string title = "Service Booking Cancelled";
const std::string message = "Service Booking of id " + bookingID + " successfully cancelled."; const std::string message = "Service Booking (ID: " + bookingID + ") has been successfully cancelled";
currentServiceBooking->setStatus(util::ServiceJobStatus::CANCELLED); currentServiceBooking->setStatus(util::ServiceJobStatus::CANCELLED);
currentTrackedServiceBooking.state = RecordState::MODIFIED; currentTrackedServiceBooking.state = RecordState::MODIFIED;
serviceBookingRemoved = true; serviceBookingRemoved = true;
@@ -1025,8 +1035,8 @@ void ServiceManagementService::updateJobStatus(const std::string& jobID)
currentJob->getBooking()->setStatus(util::ServiceJobStatus::COMPLETED); currentJob->getBooking()->setStatus(util::ServiceJobStatus::COMPLETED);
trackedServiceBookings.getValueAt(trackedServiceBookings.find(bookingId)).state = RecordState::MODIFIED; trackedServiceBookings.getValueAt(trackedServiceBookings.find(bookingId)).state = RecordState::MODIFIED;
paymentManagementService.generateInvoice(currentJob->getBooking()); paymentManagementService.generateInvoice(currentJob->getBooking());
std::string title = "Service Booking completed. Invoice Generated."; std::string title = "Service Booking Completed";
std::string message = "Services completed for the booking and invoice generated."; std::string message = "Service Booking (ID: " + bookingId + ") has been completed successfully. An invoice has been generated.";
sendNotification(currentJob->getBooking()->getCustomer(), title, message); sendNotification(currentJob->getBooking()->getCustomer(), title, message);
} }
} }
@@ -220,6 +220,13 @@ void CustomerMenu::selectService()
util::pressEnter(); util::pressEnter();
return; return;
} }
if (!verifyAllPaymentsCompleted(m_controller))
{
std::cout << "Your booking cannot be processed because you have pending payments for previous services. Please complete all outstanding invoices before booking a new service."
<< std::endl;
util::pressEnter();
return;
}
util::Vector<std::string> selectedServices; util::Vector<std::string> selectedServices;
const Service* selectedService = selectServiceFromServices(services); const Service* selectedService = selectServiceFromServices(services);
if (selectedService == nullptr) if (selectedService == nullptr)
@@ -262,6 +269,13 @@ void CustomerMenu::selectComboPackage()
util::pressEnter(); util::pressEnter();
return; return;
} }
if (!verifyAllPaymentsCompleted(m_controller))
{
std::cout << "Your booking cannot be processed because you have pending payments for previous services. Please complete all outstanding invoices before booking a new combo package."
<< std::endl;
util::pressEnter();
return;
}
const ComboPackage* selectedComboPackage = selectComboPackageFromPackages(activeComboPackages); const ComboPackage* selectedComboPackage = selectComboPackageFromPackages(activeComboPackages);
if (selectedComboPackage == nullptr) if (selectedComboPackage == nullptr)
{ {
@@ -67,7 +67,7 @@ Return type: void
*/ */
void Menu::eventListenerLoop() void Menu::eventListenerLoop()
{ {
HANDLE handles[3]; HANDLE handles[3] = { NULL, NULL, NULL };
handles[0] = m_accountDisabledEvent; handles[0] = m_accountDisabledEvent;
handles[1] = m_notificationAvailableEvent; handles[1] = m_notificationAvailableEvent;
handles[2] = m_shutdownEvent; handles[2] = m_shutdownEvent;
@@ -588,7 +588,7 @@ inline void displayInvoices(util::Map<std::string, const Invoice*> currentUserIn
<< util::getPaymentStatusString(selectedInvoice->getStatus()) << std::endl; << util::getPaymentStatusString(selectedInvoice->getStatus()) << std::endl;
std::cout << std::left << std::setw(20) << "Payment Mode:" std::cout << std::left << std::setw(20) << "Payment Mode:"
<< util::getPaymentModeString(selectedInvoice->getPaymentMethod()) << std::endl; << util::getPaymentModeString(selectedInvoice->getPaymentMethod()) << std::endl;
auto inventoryItemsInInvoice = selectedInvoice->getParts(); auto& inventoryItemsInInvoice = selectedInvoice->getParts();
if (inventoryItemsInInvoice.isEmpty()) if (inventoryItemsInInvoice.isEmpty())
{ {
std::cout << "No inventory items used.\n\n"; std::cout << "No inventory items used.\n\n";
@@ -597,7 +597,6 @@ inline void displayInvoices(util::Map<std::string, const Invoice*> currentUserIn
std::cout << "\nItems Used:\n"; std::cout << "\nItems Used:\n";
std::cout << std::left std::cout << std::left
<< std::setw(20) << "ItemName" << std::setw(20) << "ItemName"
<< std::setw(10) << "Quantity"
<< std::setw(10) << "Price" << std::setw(10) << "Price"
<< std::endl; << std::endl;
std::cout << std::string(40, '-') << std::endl; std::cout << std::string(40, '-') << std::endl;
@@ -606,7 +605,6 @@ inline void displayInvoices(util::Map<std::string, const Invoice*> currentUserIn
InventoryItem* currentItem = inventoryItemsInInvoice.getValueAt(iterator); InventoryItem* currentItem = inventoryItemsInInvoice.getValueAt(iterator);
std::cout << std::left std::cout << std::left
<< std::setw(20) << currentItem->getPartName() << std::setw(20) << currentItem->getPartName()
<< std::setw(10) << currentItem->getQuantity()
<< std::setw(10) << currentItem->getPrice() << std::setw(10) << currentItem->getPrice()
<< std::endl; << std::endl;
} }
@@ -1146,15 +1144,18 @@ inline void displayAllComboPackages(util::Map<std::string, const ComboPackage*>
for (int index = 0; index < comboPackages.getSize(); index++) for (int index = 0; index < comboPackages.getSize(); index++)
{ {
const ComboPackage* currentComboPackage = comboPackages.getValueAt(index); const ComboPackage* currentComboPackage = comboPackages.getValueAt(index);
if (currentComboPackage && currentComboPackage->getState() != util::State::ACTIVE) if (currentComboPackage)
{ {
continue; if (currentComboPackage->getState() != util::State::ACTIVE)
{
continue;
}
std::cout << std::left
<< std::setw(15) << currentComboPackage->getId()
<< std::setw(35) << util::truncateString(currentComboPackage->getPackageName(), 30)
<< std::setw(15) << util::calculateComboServiceEstimatedCost(currentComboPackage)
<< std::endl;
} }
std::cout << std::left
<< std::setw(15) << currentComboPackage->getId()
<< std::setw(35) << util::truncateString(currentComboPackage->getPackageName(), 30)
<< std::setw(15) << util::calculateComboServiceEstimatedCost(currentComboPackage)
<< std::endl;
} }
} }
@@ -1180,18 +1181,21 @@ inline const ComboPackage* selectComboPackageFromPackages(const util::Map<std::s
for (int index = 0; index < comboPackages.getSize(); index++) for (int index = 0; index < comboPackages.getSize(); index++)
{ {
const ComboPackage* currentComboPackage = comboPackages.getValueAt(index); const ComboPackage* currentComboPackage = comboPackages.getValueAt(index);
if (currentComboPackage && currentComboPackage->getState() != util::State::ACTIVE) if (currentComboPackage)
{ {
continue; if (currentComboPackage->getState() != util::State::ACTIVE)
{
continue;
}
activeComboPackages.insert(currentIndex, currentComboPackage);
std::cout << std::left
<< std::setw(10) << currentIndex
<< std::setw(15) << currentComboPackage->getId()
<< std::setw(35) << util::truncateString(currentComboPackage->getPackageName(), 30)
<< std::setw(15) << util::calculateComboServiceEstimatedCost(currentComboPackage)
<< std::endl;
currentIndex++;
} }
activeComboPackages.insert(currentIndex, currentComboPackage);
std::cout << std::left
<< std::setw(10) << currentIndex
<< std::setw(15) << currentComboPackage->getId()
<< std::setw(35) << util::truncateString(currentComboPackage->getPackageName(), 30)
<< std::setw(15) << util::calculateComboServiceEstimatedCost(currentComboPackage)
<< std::endl;
currentIndex++;
} }
if (activeComboPackages.getSize() == 0) if (activeComboPackages.getSize() == 0)
{ {
@@ -1467,3 +1471,45 @@ inline void displayNewNotification(util::Vector<const Notification*> notificatio
MB_ICONINFORMATION); MB_ICONINFORMATION);
} }
} }
/*
Function: verifyAllPaymentsCompleted
Description: Checks whether the authenticated customer has completed
all payments for their invoices. Iterates through all
invoices belonging to the customer and verifies that
each invoice has a payment status of COMPLETED.
Parameters: Controller& m_controller -
reference to the Controller object used to access
authenticated user and invoice data
Return type: bool
true if all invoices for the authenticated customer
are completed, false if any invoice is pending or not completed
Throws: std::runtime_error if no authenticated user is found
*/
inline bool verifyAllPaymentsCompleted(Controller& m_controller)
{
const User* authenticatedUser = m_controller.getAuthenticatedUser();
if (!authenticatedUser)
{
throw std::runtime_error("No authenticated user found.");
}
const std::string& authenticatedUserId = authenticatedUser->getId();
util::Map<std::string, const Invoice*> listOfInvoices = m_controller.getAllInvoices();
for (int invoiceIndex = 0; invoiceIndex < listOfInvoices.getSize(); ++invoiceIndex)
{
const Invoice* invoice = listOfInvoices.getValueAt(invoiceIndex);
if (!invoice)
{
continue;
}
const std::string& customerId = invoice->getBooking()->getCustomerId();
if (customerId == authenticatedUserId)
{
if (invoice->getStatus() != util::PaymentStatus::COMPLETED)
{
return false;
}
}
}
return true;
}