diff --git a/public/css/dashboard.css b/public/css/dashboard.css index 71fea48..c2090a2 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -1977,4 +1977,14 @@ tr[data-vaid]:hover { font-size: 12px; transform: none; color: var(--accent-light); +} + + +.field-error { + border: 1px solid var(--error-color) !important; + background-color: rgba(220, 38, 38, 0.05) !important; +} + +[data-theme="dark"] .field-error { + background-color: rgba(220, 38, 38, 0.1) !important; } \ No newline at end of file diff --git a/src/controllers/client/dashboard/clientUpdateBankDetails.controller.js b/src/controllers/client/dashboard/clientUpdateBankDetails.controller.js new file mode 100644 index 0000000..1b372ae --- /dev/null +++ b/src/controllers/client/dashboard/clientUpdateBankDetails.controller.js @@ -0,0 +1,80 @@ +import { ClientModel } from "../../../models/vaModel.js"; +import authFetch from "../../../utils/authFetch.js"; + +async function update_client_bank_details(req, res) { + + try { + const clientId = req.body.clientId; + const clientEmail = req.body.clientEmail; + const payload = req.body || {}; + const { userDetails } = payload; + + const client = await ClientModel.findById(clientId); + if (!client) { + return res.status(404).json({ + success: false, + message: "Client not found" + }); + } + + if (!userDetails) { + return res.status(400).json({ + success: false, + message: "Missing userDetails in request body" + }); + } + + userDetails._id = clientId; // Ensure the _id is set to clientId + userDetails.email = clientEmail; // Ensure the email is set to clientEmail + + // console.log("Final userDetails to update:", userDetails); + + try { + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}/services/apexrest/docva/api/client`; + + const response = await authFetch(salesforceUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ userDetails }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + } + + const result = await response.json(); + // console.log("✅ Successfully synced VA data to Salesforce:", result); + + return res.status(200).json({ + success: true, + message: "Client bank details updated and synced with Salesforce successfully", + data: result + }); + } catch (error) { + console.error("Failed to sync with Salesforce:", error.message); + console.log(error) + + // Create a custom error to distinguish Salesforce errors + const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + salesforceError.isSalesforceError = true; + throw salesforceError; + } + + } catch (error) { + if (error.message === "Client not found") { + return res.status(404).json({ + success: false, + message: error.message + }); + } + return res.status(500).json({ + success: false, + message: "Internal Server Error" + }); + } +} + +export { update_client_bank_details }; \ No newline at end of file diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js index 56b6ed1..47c608a 100644 --- a/src/routes/v1/client.routes.js +++ b/src/routes/v1/client.routes.js @@ -15,6 +15,7 @@ import { client_dashboard_projects } from "../../controllers/client/dashboard/cl import { client_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js"; import { va_tips_delete, va_tips_get, va_tips_manage_function, va_tips_update } from "../../controllers/va/vaTips/vaTipsManage.controller.js"; import { clientDashboardGetVaLogsController } from "../../controllers/client/dashboard/clientDashboardGetVaLogs.controller.js"; +import { update_client_bank_details } from "../../controllers/client/dashboard/clientUpdateBankDetails.controller.js"; // clients routes clientRoutes.post("/register", va_register_client); // Register a new VA @@ -40,6 +41,7 @@ clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_ clientRoutes.get("/dashboard-projects", clientAuthValidator, client_dashboard_projects); // VA Clients Projects Callback clientRoutes.get("/profile", clientAuthValidator, get_client_dashboard_profile); clientRoutes.patch("/profile", clientAuthValidator, update_client_password); +clientRoutes.post("/profile", clientAuthValidator, update_client_bank_details); clientRoutes.patch("/:clientId", update_client); clientRoutes.delete("/:clientId", delete_client); diff --git a/views/client/dashboard-profile.ejs b/views/client/dashboard-profile.ejs index b656b2a..cd70d08 100644 --- a/views/client/dashboard-profile.ejs +++ b/views/client/dashboard-profile.ejs @@ -568,6 +568,369 @@ } } + // Change bank details + async function changeBankDetails(event) { + event.preventDefault(); + + const formData = { + userDetails: {} + }; + + const activePaymentDetails = document.querySelector('.payment-details.active'); + if (activePaymentDetails) { + const requiredFields = activePaymentDetails.querySelectorAll('[required]'); + let isValid = true; + formData.userDetails.paymentMethod = activePaymentDetails.id === 'card-details' ? 'card' : 'bank'; + + // Validate card payment + if (formData.userDetails.paymentMethod === 'card') { + // Card number validation + const cardNumber = document.getElementById('card-number').value; + if (!validateCardNumber(cardNumber)) { + isValid = false; + showError('card-number'); + return isValid; + } else { + // Show card type + const cardType = detectCardType(cardNumber); + if (cardType !== 'unknown') { + // You can display card type to user if needed + console.log('Detected card type:', cardType); + } + } + + // Expiry date validation + const expiryDate = document.getElementById('expiry-date').value; + if (!validateExpiryDate(expiryDate)) { + isValid = false; + showError('expiry-date'); + return isValid; + } + + // CVV validation + const cvv = document.getElementById('security-code').value; + const cardType = detectCardType(cardNumber); + if (!validateCVV(cvv, cardType)) { + isValid = false; + showError('security-code'); + return isValid; + } + + // ZIP code validation + const zipCode = document.getElementById('card-zip').value; + if (!validateZipCodeRegex(zipCode)) { + isValid = false; + showError('card-zip'); + return isValid; + } + + } + // Validate bank payment + else if (formData.userDetails.paymentMethod === 'bank') { + // Account number validation + const accountNumber = document.getElementById('account-number').value; + if (!validateBankAccount(accountNumber)) { + isValid = false; + showError('account-number'); + return isValid; + } + + // Routing number validation + const routingNumber = document.getElementById('routing-number').value; + if (!validateRoutingNumber(routingNumber)) { + isValid = false; + showError('routing-number'); + return isValid; + } + + // Basic validation for other bank fields + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.classList.add('field-error'); + if (isValid === false) { + showError(field.id); + } + } else { + field.classList.remove('field-error'); + } + }); + } + } + + let accountCardNumber = document.getElementById('card-number'); + let accountExpiryDate = document.getElementById('expiry-date'); + let accountSecurityCode = document.getElementById('security-code'); + let accountCardZip = document.getElementById('card-zip'); + let accountBankName = document.getElementById('bank-name'); + let accountAccountNumber = document.getElementById('account-number'); + let accountRoutingNumber = document.getElementById('routing-number'); + let accountAccountType = document.getElementById('account-type'); + let accountAccountHolderName = document.getElementById('account-holder-name'); + + if(accountCardNumber.value) { + formData.userDetails.cardNumber = accountCardNumber.value; + } + + if(accountExpiryDate.value) { + formData.userDetails.expiryDate = accountExpiryDate.value; + } + + if(accountSecurityCode.value) { + formData.userDetails.securityCode = accountSecurityCode.value; + } + + if(accountCardZip.value) { + formData.userDetails.cardZip = accountCardZip.value; + } + + if(accountBankName.value) { + formData.userDetails.bankName = accountBankName.value; + } + + if(accountAccountNumber.value) { + formData.userDetails.accountNumber = accountAccountNumber.value; + } + + if(accountRoutingNumber.value) { + formData.userDetails.routingNumber = accountRoutingNumber.value; + } + + if(accountAccountType.value) { + formData.userDetails.accountType = accountAccountType.value; + } + + if(accountAccountHolderName.value) { + formData.userDetails.accountHolderName = accountAccountHolderName.value; + } + + try { + const saveBtn = document.getElementById('saveBankDetailsBtn'); + saveBtn.disabled = true; + saveBtn.textContent = 'Updatind Details...'; + + const response = await fetchWithAuth(`${API_BASE}/profile`, { + method: 'POST', + body: JSON.stringify(formData) + }); + + if (!response) { + saveBtn.disabled = false; + saveBtn.textContent = 'Update Bank Details'; + return; + } + + const result = await response.json(); + if (result.success) { + alert('Bank details updated successfully!'); + closeBankDetailsModal(); + } else { + alert(result.message || 'Failed to update bank details'); + } + } catch (error) { + console.error('Error changing Bank Details:', error); + alert('Error changing bank details'); + } finally { + const saveBtn = document.getElementById('saveBankDetailsBtn'); + saveBtn.disabled = false; + saveBtn.textContent = 'Update Bank Details'; + } + } + + // Bank account validation + function validateBankAccount(accountNumber) { + return /^\d{8,17}$/.test(accountNumber.replace(/\s/g, '')); + } + + // Routing number validation (basic US routing number check) + function validateRoutingNumber(routingNumber) { + const cleanRouting = routingNumber.replace(/\s/g, ''); + + if (!/^\d{9}$/.test(cleanRouting)) { + return false; + } + + // Check for invalid patterns + if (isInvalidRoutingNumberPattern(cleanRouting)) { + return false; + } + + // ABA routing number checksum validation + return isValidABAChecksum(cleanRouting); + } + + function isInvalidRoutingNumberPattern(routingNumber) { + const invalidPatterns = [ + /^0+$/, // All zeros + /^1+$/, // All ones + /^2+$/, // All twos + /^3+$/, // All threes + /^4+$/, // All fours + /^5+$/, // All fives + /^6+$/, // All sixes + /^7+$/, // All sevens + /^8+$/, // All eights + /^9+$/, // All nines + /^012345678/, // Sequential + /^876543210/, // Reverse sequential + ]; + + return invalidPatterns.some(pattern => pattern.test(routingNumber)); + } + + function isValidABAChecksum(routingNumber) { + const digits = routingNumber.split('').map(Number); + + // ABA routing number checksum formula + const sum = (3 * (digits[0] + digits[3] + digits[6])) + + (7 * (digits[1] + digits[4] + digits[7])) + + (1 * (digits[2] + digits[5] + digits[8])); + + return sum % 10 === 0; + } + + + // Expiry date validation + function validateExpiryDate(expiryDate) { + const expiryRegex = /^(0[1-9]|1[0-2])\/([0-9]{2})$/; + if (!expiryRegex.test(expiryDate)) { + return false; + } + + const [month, year] = expiryDate.split('/'); + const expiry = new Date(2000 + parseInt(year), parseInt(month)); + const currentDate = new Date(); + const currentMonth = new Date(currentDate.getFullYear(), currentDate.getMonth()); + + return expiry >= currentMonth; + } + + // CVV validation + function validateCVV(cvv, cardType) { + if (!/^\d+$/.test(cvv)) { + return false; + } + + const cvvLengths = { + visa: 3, + mastercard: 3, + amex: 4, + discover: 3, + diners: 3, + jcb: 3 + }; + + const expectedLength = cvvLengths[cardType] || 3; + return cvv.length === expectedLength; + } + + // ZIP code validation + function validateZipCodeRegex(zipCode) { + return /^\d{5}(-\d{4})?$/.test(zipCode); + } + + function validateCardNumber(cardNumber) { + const cleanNumber = cardNumber.replace(/[\s-]/g, ''); + + // Check if it's all digits and has proper length + if (!/^\d+$/.test(cleanNumber) || cleanNumber.length < 13 || cleanNumber.length > 19) { + return false; + } + + // Check for invalid patterns + if (isInvalidCardPattern(cleanNumber)) { + return false; + } + + // Check for invalid BIN ranges + if (hasInvalidBIN(cleanNumber)) { + return false; + } + + // Luhn algorithm + return isValidLuhn(cleanNumber); + } + + // Enhanced Luhn algorithm + function isValidLuhn(cardNumber) { + let sum = 0; + let isEven = false; + + for (let i = cardNumber.length - 1; i >= 0; i--) { + let digit = parseInt(cardNumber.charAt(i)); + + if (isEven) { + digit *= 2; + if (digit > 9) { + digit -= 9; + } + } + + sum += digit; + isEven = !isEven; + } + + return sum % 10 === 0; + } + + // Function to check for invalid card patterns + function isInvalidCardPattern(cardNumber) { + const invalidPatterns = [ + /^0+$/, // All zeros + /^1+$/, // All ones + /^2+$/, // All twos + /^3+$/, // All threes + /^4+$/, // All fours + /^5+$/, // All fives + /^6+$/, // All sixes + /^7+$/, // All sevens + /^8+$/, // All eights + /^9+$/, // All nines + /^0123/, // Sequential + /^1234/, // Sequential + ]; + + return invalidPatterns.some(pattern => pattern.test(cardNumber)); + } + + // Function to check for invalid BIN ranges + function hasInvalidBIN(cardNumber) { + const firstSix = cardNumber.substring(0, 6); + const firstFour = cardNumber.substring(0, 4); + const firstTwo = cardNumber.substring(0, 2); + + // Invalid BIN ranges + const invalidBINs = [ + '000000', '000001', '000002', '000003', // Test cards should be handled separately + '111111', '222222', '333333', '444444', + '555555', '666666', '777777', '888888', + '999999', '123456', '654321' + ]; + + // Check specific invalid ranges + if (invalidBINs.includes(firstSix)) { + return true; + } + + // Check for known invalid starting digits + if (['0', '3', '6', '7', '8'].includes(firstTwo)) { + // 0 - Reserved + // 3 - Should only be 34, 37 (AMEX) + // 6 - Should follow Discover patterns + // 7, 8 - Not currently assigned to major networks + if (!isValidCardPrefix(cardNumber)) { + return true; + } + } + + return false; + } + + function isValidCardPrefix(cardNumber) { + const type = detectCardType(cardNumber); + return type !== 'unknown'; + } + // Logout Modal Functions function openLogoutModal() { const modal = document.getElementById('logoutModal'); @@ -592,6 +955,7 @@ // Password form submission document.getElementById('passwordForm').addEventListener('submit', changePassword); + document.getElementById('payment-form').addEventListener('submit', changeBankDetails); // Close modal when clicking outside document.getElementById('passwordModal').addEventListener('click', function(e) { @@ -1253,6 +1617,20 @@ }); + function showError(fieldId = null) { + // Highlight specific field if provided + if (fieldId) { + const field = document.getElementById(fieldId); + if (field) { + field.classList.add('field-error'); + // Remove error after 5 seconds + setTimeout(() => { + field.classList.remove('field-error'); + }, 5000); + } + } + } + \ No newline at end of file