diff --git a/efile_app/efile/api/filing_views.py b/efile_app/efile/api/filing_views.py
index 48f50ea..a62a72e 100644
--- a/efile_app/efile/api/filing_views.py
+++ b/efile_app/efile/api/filing_views.py
@@ -12,7 +12,7 @@
from django.views.decorators.http import require_http_methods
from ..utils.case_data_utils import get_case_data
-from ..utils.proxy_connection import get_party_type_code_from_api
+from ..utils.proxy_connection import get_headers, get_party_type_code_from_api
from .base import APIResponseMixin
logger = logging.getLogger(__name__)
@@ -84,6 +84,85 @@ def create_filing(request):
except Exception as e:
return FilingAPIViews.error_response(f"Error: {str(e)}")
+ @staticmethod
+ @require_http_methods(["POST"])
+ @csrf_exempt
+ def payment_fees(request):
+ try:
+ logger.info("Test in payment fees!")
+ data = json.loads(request.body)
+ efile_data = data.get("efile_data", {})
+ if not efile_data:
+ return JsonResponse({"success": False, "error": "No efile data provided in request"}, status=400)
+
+ jurisdiction_id = request.session.get("jurisdiction")
+ auth_tokens = request.session.get("auth_tokens", {})
+ case_data = request.session.get("case_data", {})
+ court_id = case_data.get("court", "")
+ url = f"{settings.EFSP_URL}/jurisdictions/{jurisdiction_id}/filingreview/courts/{court_id}/filing/fees"
+
+ headers = get_headers()
+ tyler_token = (
+ auth_tokens.get(f"TYLER-TOKEN-{jurisdiction_id.upper()}")
+ or auth_tokens.get(f"tyler_token_{jurisdiction_id}")
+ or auth_tokens.get(f"tyler-token-{jurisdiction_id}")
+ )
+
+ # Add Tyler token if available (following auth_views.py pattern)
+ if tyler_token:
+ headers[f"TYLER-TOKEN-{jurisdiction_id.upper()}"] = tyler_token
+ else:
+ logger.warning(f"No Tyler token found for jurisdiction '{jurisdiction_id}' in filing submission")
+
+ logger.info(f"Making request!: {url}")
+ response = requests.post(url, json=efile_data, headers=headers)
+ logger.info(f"Made request: {response.status_code}")
+
+ if response.status_code == 200 or response.status_code == 201:
+ response_data = response.json()
+
+ logger.info(f"Sending back: {response_data}")
+ return JsonResponse(
+ {
+ "success": True,
+ "message": "Payment fees submitted successfully",
+ "api_response": response_data,
+ }
+ )
+ else:
+ try:
+ error_data = response.json()
+ error_message = error_data.get("error", f"API returned status {response.status_code}")
+ logger.info(f"Sending back: {error_data}, {error_message}")
+
+ # For 400 errors, include more details
+ if response.status_code == 400:
+ validation_errors = error_data.get("validation_errors", error_data.get("errors", []))
+ if validation_errors:
+ error_message += f" - Validation errors: {validation_errors}"
+
+ except json.JSONDecodeError:
+ error_message = f"API returned status {response.status_code} - Response: {response.text}"
+ except Exception as parse_error:
+ error_message = (
+ f"API returned status {response.status_code} - Could not parse response: {str(parse_error)}"
+ )
+
+ return JsonResponse(
+ {
+ "success": False,
+ "error": f"Filing submission failed: {error_message}",
+ "api_status_code": response.status_code,
+ "api_response": response.text[:500] if response.text else "No response body",
+ },
+ status=response.status_code,
+ )
+
+ except json.JSONDecodeError:
+ return FilingAPIViews.error_response("Invalid JSON data")
+ except Exception as e:
+ return FilingAPIViews.error_response(f"Error: {str(e)}")
+
@staticmethod
@require_http_methods(["GET"])
def get_filing_detail(request, filing_id):
@@ -449,6 +528,7 @@ def determine_party_type_for_existing_case(case_data):
# Individual view functions for URL mapping
get_filings = FilingAPIViews.get_filings
create_filing = FilingAPIViews.create_filing
+payment_fees = FilingAPIViews.payment_fees
get_filing_detail = FilingAPIViews.get_filing_detail
update_filing = FilingAPIViews.update_filing
delete_filing = FilingAPIViews.delete_filing
diff --git a/efile_app/efile/api/urls.py b/efile_app/efile/api/urls.py
index 0aeaca0..1213d19 100644
--- a/efile_app/efile/api/urls.py
+++ b/efile_app/efile/api/urls.py
@@ -23,7 +23,7 @@
get_optional_services,
get_party_types,
)
-from .filing_views import create_filing, delete_filing, get_filing_detail, get_filings, update_filing
+from .filing_views import create_filing, delete_filing, get_filing_detail, get_filings, payment_fees, update_filing
from .s3_upload import (
mock_s3_upload,
simple_s3_upload,
@@ -60,6 +60,7 @@
path("auth/tyler-token/", tyler_token, name="tyler_token"),
# Payment API endpoints
path("payment-accounts/", payment_accounts, name="payment_accounts"),
+ path("payment-fees/", payment_fees, name="payment_fees"),
# Filing API endpoints
path("filings/", get_filings, name="filings_list"),
path("filings/create/", create_filing, name="create_filing"),
diff --git a/efile_app/efile/static/js/api-utils.js b/efile_app/efile/static/js/api-utils.js
index faf62cd..a8c2247 100644
--- a/efile_app/efile/static/js/api-utils.js
+++ b/efile_app/efile/static/js/api-utils.js
@@ -70,7 +70,6 @@ class ApiUtils {
const cacheEntry = this.cache[cacheKey];
if (this.isCacheValid(cacheEntry)) {
- const age = Date.now() - cacheEntry.timestamp;
return cacheEntry.data;
}
@@ -273,6 +272,32 @@ class ApiUtils {
return response;
}
+ async cachedPost(endpoint, body) {
+ // Check cache first
+ const cachedResponse = this.getCachedResponse(endpoint, body);
+ if (cachedResponse !== null) {
+ return cachedResponse;
+ }
+
+ let headers = {
+ "Content-Type": "application/json",
+ "X-CSRFToken": apiUtils.getCSRFToken(),
+ };
+
+ // Make API request if not cached
+ const response = await this.makeRequest(endpoint, {
+ method: "POST",
+ headers,
+ data: body,
+ });
+
+ // Cache the response
+ this.setCachedResponse(endpoint, body, response);
+
+ return response;
+
+ }
+
async post(endpoint, data = {}, params = {}) {
return this.makeRequest(endpoint, {
method: 'POST',
diff --git a/efile_app/efile/static/js/payment.js b/efile_app/efile/static/js/payment.js
new file mode 100644
index 0000000..1c120d3
--- /dev/null
+++ b/efile_app/efile/static/js/payment.js
@@ -0,0 +1,626 @@
+/**
+ * Review Page JavaScript - Optimized Version
+ * Handles review page functionality with improved organization and performance
+ */
+
+// Configuration constants
+const CONFIG = {
+ URLS: {
+ PROFILE: '/api/auth/profile/',
+ PAYMENT_ACCOUNTS: '/api/payment-accounts/',
+ TYLER_TOKEN: '/api/auth/tyler-token/',
+ SUBMIT_FILING: '/api/submit-final-filing/',
+ QUERY_FEES: '/api/payment-fees/',
+ }
+};
+
+
+// Utility functions
+const Utils = {
+ getElement(id) {
+ return document.getElementById(id);
+ },
+
+ getElements(selector) {
+ return document.querySelectorAll(selector);
+ },
+
+ parseJSON(elementId) {
+ const element = this.getElement(elementId);
+ return element ? JSON.parse(element.textContent) : {};
+ },
+
+ showElement(element) {
+ if (element) element.style.display = "block";
+ },
+
+ hideElement(element) {
+ if (element) element.style.display = "none";
+ },
+
+ // URL parameter helper
+ getURLParam(param) {
+ return new URLSearchParams(window.location.search).get(param);
+ },
+
+ cleanURL() {
+ window.history.replaceState({}, document.title, window.location.pathname);
+ }
+
+};
+
+// Message handling
+const Messages = {
+ show(type, message) {
+ const messageDiv = Utils.getElement(type === 'error' ? 'errorMessage' : 'successMessage');
+ const textElement = Utils.getElement(type === 'error' ? 'errorText' : 'successText');
+
+ if (messageDiv && textElement) {
+ textElement.textContent = message;
+ Utils.showElement(messageDiv);
+ messageDiv.scrollIntoView({
+ behavior: "smooth",
+ block: "center"
+ });
+ }
+ },
+
+ showError(message) {
+ this.show('error', message);
+ },
+
+ showSuccess(message) {
+ this.show('success', message);
+ },
+
+ hide() {
+ Utils.hideElement(Utils.getElement('errorMessage'));
+ Utils.hideElement(Utils.getElement('successMessage'));
+ }
+};
+
+// UI Field management
+const FieldManager = {
+ // Consolidated field setting logic
+ setFieldValue(fieldPrefix, value, displayValue = value) {
+ const input = Utils.getElement(`${fieldPrefix}Input`);
+ const text = Utils.getElement(`${fieldPrefix}Text`);
+
+ if (input) input.value = value || "";
+ if (text) text.textContent = displayValue || "";
+ },
+
+ getFieldValue(fieldPrefix) {
+ const input = Utils.getElement(`${fieldPrefix}Input`);
+ const text = Utils.getElement(`${fieldPrefix}Text`);
+ const inputVal = input?.value?.trim();
+ const textVal = text?.textContent?.trim();
+ return (inputVal && inputVal.length > 0) ? inputVal : textVal;
+ },
+
+ startEditing(input, text, button, inputId) {
+ Utils.hideElement(text);
+ Utils.showElement(input);
+ input.focus();
+ input.select();
+ button.textContent = "Save";
+ button.onclick = () => this.saveField(input, text, button, inputId);
+ },
+
+ saveField(input, text, button, inputId) {
+ text.textContent = input.value.trim();
+ Utils.hideElement(input);
+ Utils.showElement(text);
+ button.textContent = "Edit";
+ button.onclick = () => this.toggleEdit(inputId, button);
+ }
+};
+
+// API handlers
+const APIHandlers = {
+ async loadPaymentAccounts() {
+ const params = {
+ jurisdiction: apiUtils.getCurrentJurisdiction()
+ };
+ const result = await apiUtils.get(CONFIG.URLS.PAYMENT_ACCOUNTS, params);
+
+ if (result?.success && result.data) {
+ UIUpdater.updatePaymentMethodsSection(result.data);
+ let elems = document.querySelectorAll('input[name="paymentMethod"]');
+ elems.forEach(e => e.addEventListener("change", () => {
+ window.queryFees();
+ }));
+ window.queryFees();
+ } else {
+ UIUpdater.showAddNewPaymentMethod();
+ }
+ }
+};
+
+// UI updaters
+const UIUpdater = {
+ updatePaymentMethodsSection(paymentAccounts) {
+ const container = Utils.getElement('paymentMethodsContainer');
+ if (!container || !paymentAccounts?.length) {
+ return this.showAddNewPaymentMethod();
+ }
+
+ let html = '
';
+
+ let hasMultipleWaivers = paymentAccounts.filter((account) => account.paymentAccountTypeCode === "WV").length > 1;
+ paymentAccounts.forEach((account, index) => {
+ const isDefault = index === 0;
+ const cardType = account.cardType?.value || "Card";
+ const cardLast4 = account.cardLast4 || "****";
+ let paymentText = `${cardType} ending in ${cardLast4}`;
+
+ if (account.paymentAccountTypeCode === "WV") {
+ if (hasMultipleWaivers) {
+ paymentText = `Payment Waiver (named "${account.accountName}")`;
+ } else {
+ paymentText = 'Payment Waiver';
+ }
+ }
+
+ html += `
`;
+ });
+
+ html += `
+
+
+
`;
+
+ container.innerHTML = html;
+ },
+
+ showAddNewPaymentMethod() {
+ const container = Utils.getElement('paymentMethodsContainer');
+ if (!container) return;
+
+ container.innerHTML = `
+
+
+ No payment methods found. Please add a payment method to continue.
+
+
+
`;
+ }
+};
+
+// Payment handling
+const PaymentHandler = {
+ async addNewPaymentMethod() {
+ try {
+ const params = new URLSearchParams({
+ jurisdiction: apiUtils.getCurrentJurisdiction(),
+ });
+ const authData = await apiUtils.get(CONFIG.URLS.TYLER_TOKEN, params);
+
+ if (!authData?.success || !authData.data?.tyler_token) {
+ Messages.showError("Authentication failed. Please try again.");
+ return;
+ }
+
+ this.redirectToPaymentForm(authData.data);
+ } catch (error) {
+ console.warn("Create payment error: %o", error);
+ Messages.showError("Failed to create payment method. Please try again.");
+ }
+ },
+
+ redirectToPaymentForm(authData) {
+ const form = document.createElement("form");
+ form.method = "post";
+
+ const jurisdiction = authData.state || apiUtils.getCurrentJurisdiction();
+ form.action = Utils.parseJSON('new-toga-url');
+
+ let dateStr = new Date().toDateString();
+ const fields = [
+ ['account_name', `Payment Account made on ${dateStr}`],
+ ['global', 'false'],
+ ['type_code', 'CC'],
+ ['tyler_info', authData.tyler_token],
+ ['original_url', `${window.location.origin}/jurisdiction/${jurisdiction}/payment/?payment_status=success`],
+ ['error_url', `${window.location.origin}/jurisdiction/${jurisdiction}/payment/?payment_status=failure`]
+ ];
+
+ fields.forEach(([name, value]) => {
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = name;
+ input.value = value;
+ form.appendChild(input);
+ });
+
+ document.body.appendChild(form);
+ form.submit();
+ },
+
+ handleCallback() {
+ const status = Utils.getURLParam('payment_status');
+
+ if (status === 'success') {
+ Messages.showSuccess(gettext("Payment method added successfully!"));
+ setTimeout(() => APIHandlers.loadPaymentAccounts(), 1000);
+ Utils.cleanURL();
+ } else if (status === 'failure') {
+ Messages.showError(gettext("Failed to add payment method. Please try again."));
+ Utils.cleanURL();
+ }
+ },
+
+ calcPaymentCosts() {
+
+ }
+};
+
+// Navigation
+const Navigation = {
+ goBack() {
+ window.location.href = `/jurisdiction/${apiUtils.getCurrentJurisdiction()}/upload`;
+ },
+
+ async toReview() {
+ const selectedPaymentMethod = document.querySelector('input[name="paymentMethod"]:checked');
+ await apiUtils.saveCaseData({
+ "selected_payment_account": selectedPaymentMethod.value,
+ "selected_payment_account_name": selectedPaymentMethod.getAttribute("fullName")
+ });
+
+ window.location.href = `/jurisdiction/${apiUtils.getCurrentJurisdiction()}/review`;
+ }
+};
+
+// Filing submission
+const FilingHandler = {
+ async queryFees() {
+ document.getElementById("paymentSection").setAttribute("hidden", true);
+ this.setFeesState(true);
+
+ const userData = await this.collectUserData();
+ const selectedPaymentMethod = document.querySelector('input[name="paymentMethod"]:checked');
+ if (!selectedPaymentMethod) {
+ this.setFeesState(false);
+ return;
+ }
+
+
+ try {
+ const result = await this.processFees(userData, selectedPaymentMethod.value);
+ this.handleFeesResponse(result);
+ } catch (error) {
+ console.warn("Error on submission: %o", error)
+ Messages.showError(gettext("An unexpected error occurred. Please try again."));
+ this.setFeesState(false);
+ }
+ },
+
+ async collectUserData() {
+ const params = {
+ jurisdiction: apiUtils.getCurrentJurisdiction()
+ };
+ const data = await apiUtils.get(CONFIG.URLS.PROFILE, params, true);
+
+ if (data?.success && data.data) {
+ const profile = data.data;
+ const fullName = [profile.first_name, profile.last_name].filter(n => n).join(" ");
+
+ // Set all user fields
+ return {
+ fullName: fullName,
+ address: profile.address,
+ addressLine2: profile.address_line2,
+ city: profile.city,
+ state: profile.state,
+ zip: profile.zip,
+ email: profile.email,
+ phone: profile.phone
+ };
+ }
+ return {
+ fullName: "",
+ address: "",
+ addressLine2: "",
+ city: "Citytown",
+ state: "IL",
+ zip: "",
+ email: "test@example.com",
+ phone: "",
+ };
+ },
+
+ setFeesState(isQueryingFees) {
+ const submitButton = Utils.getElement('submitButton');
+ const loadingSpinner = Utils.getElement('loadingSpinner');
+
+ if (submitButton) submitButton.disabled = isQueryingFees;
+ if (loadingSpinner) loadingSpinner.style.display = isQueryingFees ? "block" : "none";
+ },
+
+ async processFees(userData, paymentAccountID) {
+ let [caseData, uploadData] = await Promise.all([
+ apiUtils.getCaseData(),
+ apiUtils.getUploadData()
+ ]);
+
+ caseData = caseData.data.case_data;
+
+ const efilingData = this.buildEFilingData(userData, caseData, uploadData, paymentAccountID);
+
+ return await apiUtils.cachedPost(CONFIG.URLS.QUERY_FEES, {
+ efile_data: efilingData,
+ confirm_submission: true,
+ payment_account_id: paymentAccountID
+ });
+ },
+
+
+ buildEFilingData(userData, caseData, uploadData, paymentAccountID) {
+ const nameParts = userData.fullName.split(" ");
+ const firstName = nameParts[0] || "";
+ const lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : "";
+ const middleName = nameParts.length > 2 ? nameParts.slice(1, -1).join(" ") : "";
+
+ const partyType = caseData.determined_party_type || caseData.petitioner_party_type || caseData.party_type;
+
+ if (!partyType) {
+ throw new Error('Party type could not be determined. This is required for eFiling.');
+ }
+
+ // Build user object
+ const mainUser = {
+ mobile_number: userData.phone,
+ phone_number: userData.phone,
+ address: {
+ address: userData.address,
+ unit: userData.addressLine2,
+ city: userData.city,
+ state: userData.state,
+ zip: userData.zip,
+ country: "US"
+ },
+ email: userData.email,
+ party_type: partyType,
+ date_of_birth: "",
+ is_form_filler: true,
+ name: {
+ first: firstName,
+ middle: middleName,
+ last: lastName,
+ suffix: ""
+ },
+ is_new: true
+ };
+
+ const users = [mainUser];
+
+ // Add second user if needed for name changes
+ if (caseData.new_name_party_type) {
+ users.push({
+ ...mainUser,
+ party_type: caseData.new_name_party_type,
+ name: {
+ first: caseData.new_first_name || firstName,
+ middle: caseData.new_middle_name || middleName,
+ last: caseData.new_last_name || lastName,
+ suffix: caseData.new_suffix || ""
+ }
+ });
+ }
+
+ // Add second user if needed for name changes
+ if (caseData.respondent_name_party_type) {
+ users.push({
+ party_type: caseData.respondent_name_party_type,
+ name: {
+ first: caseData.respondent_first_name || "",
+ middle: caseData.respondent_middle_name || "",
+ last: caseData.respondent_last_name || "",
+ suffix: caseData.respondent_suffix || ""
+ },
+ is_new: true,
+ });
+ }
+
+ let other_parties = [];
+
+ if (caseData.other_first_name && caseData.other_party_type) {
+ other_parties.push({
+ party_type: caseData.other_party_type,
+ name: {
+ first: caseData.other_first_name,
+ last: caseData.other_last_name
+ },
+ address: {
+ address: caseData.other_address_line_1,
+ unit: caseData.other_address_line_2,
+ city: caseData.other_address_city,
+ state: caseData.other_address_state,
+ zip: caseData.other_address_zip,
+ country: "US"
+ },
+ email: caseData.other_email,
+ phone_number: caseData.other_phone_number,
+ is_new: true,
+ });
+ }
+
+ const efilingData = {
+ efile_case_category: caseData.case_category,
+ efile_case_type: caseData.case_type,
+ efile_case_subtype: caseData.case_subtype,
+ previous_case_id: caseData?.previous_case_id,
+ docket_number: caseData?.docket_number,
+ users,
+ other_parties,
+ user_started_case: !caseData?.previous_case_id,
+ al_court_bundle: [],
+ cross_references: "",
+ comments_to_clerk: "",
+ tyler_payment_id: paymentAccountID,
+ lead_contact: {
+ name: {
+ first: firstName,
+ middle: middleName,
+ last: lastName
+ },
+ email: userData.email
+ },
+ return_date: ""
+ };
+
+ // Add court bundles for documents
+ this.addCourtBundles(efilingData, uploadData, caseData, users);
+
+ return efilingData;
+ },
+
+ addCourtBundles(efilingData, uploadData, caseData, users) {
+ const courtName = caseData.court_name || caseData.court || "";
+ if (courtName.toLowerCase().includes("cook") || courtName.toLowerCase().includes("dupage")) {
+ efilingData.cross_references = {
+ 254500: "254500"
+ };
+ }
+
+ // Add lead document
+ if (uploadData?.files?.lead) {
+ const leadBundle = this.createDocumentBundle(
+ uploadData.files.lead,
+ uploadData.lead_filing_type || caseData.filing_type,
+ uploadData.lead_document_type || caseData.document_type,
+ uploadData.lead_filing_component || caseData.filing_component,
+ users,
+ uploadData.lead_filing_type_name || caseData.case_type_name,
+ uploadData.lead_document_type_name || "",
+ uploadData.lead_cc_email
+ );
+ efilingData.al_court_bundle.push(leadBundle);
+ }
+
+ // Add supporting documents
+ if (uploadData?.files?.supporting?.length > 0) {
+ uploadData.files.supporting.forEach((doc, index) => {
+ const config = uploadData.supporting_documents?.[index] || {};
+ const bundle = this.createDocumentBundle(
+ doc,
+ config.filing_type || caseData.filing_type_id,
+ config.document_type || caseData.document_type,
+ config.filing_component || "supporting",
+ users,
+ config.filing_type_name || `Supporting Document ${index + 1}`,
+ config.document_type_name || "",
+ config.cc_email
+ );
+ efilingData.al_court_bundle.push(bundle);
+ });
+ }
+ },
+
+ createDocumentBundle(doc, filingType, documentType, filingComponent, users, description, docDescription, cc_email) {
+ if (cc_email) {
+ courtesy_copies = [cc_email]
+ } else {
+ courtesy_copies = []
+ }
+ return {
+ proxy_enabled: true,
+ filing_type: filingType,
+ optional_services: [],
+ due_date: null,
+ filing_description: description,
+ reference_number: "",
+ filing_attorney: "",
+ filing_comment: "",
+ courtesy_copies: courtesy_copies,
+ preliminary_copies: [],
+ filing_parties: users.length === 1 ? ["users[0]"] : ["users[0]", "users[1]"],
+ filing_action: "efile",
+ tyler_merge_attachments: false,
+ document_type: documentType,
+ filing_component: filingComponent,
+ filename: doc.name,
+ document_description: docDescription,
+ data_url: doc.url || doc.s3_url || doc.file_url || doc.download_url
+ };
+ },
+
+ handleSubmissionResult(result) {
+ if (result?.success) {
+ Messages.showSuccess(gettext("Filing submitted successfully! You will be redirected to the confirmation page."));
+ setTimeout(() => {
+ const jurisdiction = apiUtils.getCurrentJurisdiction();
+ window.location.href = result.redirect_url || `/jurisdiction/${jurisdiction}/filing-confirmation/`;
+ }, 2000);
+ } else {
+ Messages.showError(result?.error || "An error occurred during submission.");
+ this.setSubmissionState(false);
+ }
+ },
+
+ handleFeesResponse(result) {
+ if (result?.success) {
+ let htmlStr = `
+ Total: $${result.api_response.feesCalculationAmount.value}
+
+
+ `;
+ for (let specificFee of result.api_response.allowanceCharge) {
+ if (specificFee.chargeIndicator.value) {
+ htmlStr += `- ${specificFee.allowanceChargeReason.value}: $${specificFee.amount.value}
`;
+ }
+ }
+ htmlStr += "
";
+
+ let infoElem = document.getElementById("paymentInfo");
+ infoElem.innerHTML = htmlStr;
+ document.getElementById("paymentSection").removeAttribute("hidden");
+ } else {
+ Messages.showError(result?.error || "An error occurred when calculating fees.");
+ }
+ this.setFeesState(false);
+ }
+};
+
+// Main application initialization
+const ReviewApp = {
+ async init() {
+ await this.loadAllData();
+ PaymentHandler.handleCallback();
+ },
+
+ async loadAllData() {
+ // Load other data in parallel
+ await Promise.all([
+ APIHandlers.loadPaymentAccounts()
+ ]);
+ }
+};
+
+// Global function exports for HTML onclick handlers
+window.goBack = Navigation.goBack;
+window.toReview = Navigation.toReview;
+window.queryFees = FilingHandler.queryFees.bind(FilingHandler);
+window.PaymentHandler = PaymentHandler;
+window.Navigation = Navigation;
+
+// Initialize app when DOM is ready
+document.addEventListener("DOMContentLoaded", () => ReviewApp.init());
\ No newline at end of file
diff --git a/efile_app/efile/static/js/review.js b/efile_app/efile/static/js/review.js
index 8bb525f..18fca25 100644
--- a/efile_app/efile/static/js/review.js
+++ b/efile_app/efile/static/js/review.js
@@ -17,6 +17,7 @@ const CONFIG = {
TYLER_TOKEN: '/api/auth/tyler-token/',
SUBMIT_FILING: '/api/submit-final-filing/',
CASE_DATA: '/api/get-case-data/',
+ QUERY_FEES: '/api/payment-fees/',
}
};
@@ -58,15 +59,6 @@ const Utils = {
const cleaned = phone.replace(/[\s()-\.]/g, "");
return CONFIG.VALIDATION.PHONE_REGEX.test(cleaned);
},
-
- // URL parameter helper
- getURLParam(param) {
- return new URLSearchParams(window.location.search).get(param);
- },
-
- cleanURL() {
- window.history.replaceState({}, document.title, window.location.pathname);
- }
};
// Message handling
@@ -263,10 +255,10 @@ const APIHandlers = {
},
async loadUserInfo() {
- const params = new URLSearchParams({
- jurisdiction: apiUtils.getCurrentJurisdiction(),
- });
- const data = await DataManager.fetchJSON(`${CONFIG.URLS.PROFILE}?${params}`);
+ const params = {
+ jurisdiction: apiUtils.getCurrentJurisdiction()
+ };
+ const data = await apiUtils.get(CONFIG.URLS.PROFILE, params);
if (data?.success && data.data) {
const profile = data.data;
@@ -300,14 +292,12 @@ const APIHandlers = {
},
async loadPaymentAccounts() {
- const params = new URLSearchParams({
- jurisdiction: apiUtils.getCurrentJurisdiction(),
- });
- const result = await DataManager.fetchJSON(`${CONFIG.URLS.PAYMENT_ACCOUNTS}?${params}`);
-
- if (result?.success && result.data) {
- UIUpdater.updatePaymentMethodsSection(result.data);
- } else {
+ try {
+ const caseData = await DataManager.getCaseData();
+ UIUpdater.updatePaymentMethodsSection(caseData.selected_payment_account, caseData.selected_payment_account_name || "Your payment");
+ await window.queryFees();
+ } catch (ex) {
+ console.log(ex);
UIUpdater.showAddNewPaymentMethod();
}
}
@@ -373,51 +363,31 @@ const UIUpdater = {
`;
},
- updatePaymentMethodsSection(paymentAccounts) {
+ updatePaymentMethodsSection(account, account_name) {
+ console.log(account);
const container = Utils.getElement('paymentMethodsContainer');
- if (!container || !paymentAccounts?.length) {
- return this.showAddNewPaymentMethod();
- }
-
let html = '';
- let hasMultipleWaivers = paymentAccounts.filter((account) => account.paymentAccountTypeCode === "WV").length > 1;
- paymentAccounts.forEach((account, index) => {
- const isDefault = index === 0;
- const cardType = account.cardType?.value || "Card";
- const cardLast4 = account.cardLast4 || "****";
- let paymentText = `${cardType} ending in ${cardLast4}`;
-
- if (account.paymentAccountTypeCode === "WV") {
- if (hasMultipleWaivers) {
- paymentText = `Payment Waiver (named "${account.accountName}")`;
- } else {
- paymentText = 'Payment Waiver';
- }
- }
+ //const cardType = account.cardType?.value || "Card";
+ //const cardLast4 = account.cardLast4 || "****";
+ //let paymentText = `${cardType} ending in ${cardLast4}`;
- html += `
+ html += `
-
-
-
`;
+ html += `
`;
container.innerHTML = html;
},
@@ -429,84 +399,16 @@ const UIUpdater = {
container.innerHTML = `
`;
}
};
-// Payment handling
-const PaymentHandler = {
- async addNewPaymentMethod() {
- try {
- const params = new URLSearchParams({
- jurisdiction: apiUtils.getCurrentJurisdiction(),
- });
- const authData = await DataManager.fetchJSON(`${CONFIG.URLS.TYLER_TOKEN}?${params}`);
-
- if (!authData?.success || !authData.data?.tyler_token) {
- Messages.showError("Authentication failed. Please try again.");
- return;
- }
-
- this.redirectToPaymentForm(authData.data);
- } catch (error) {
- console.log("Create payment error: %o", error);
- Messages.showError("Failed to create payment method. Please try again.");
- }
- },
-
- redirectToPaymentForm(authData) {
- const form = document.createElement("form");
- form.method = "post";
-
- const jurisdiction = authData.state || apiUtils.getCurrentJurisdiction();
- // TODO: we should revisit this hardcoded value in the future too
- form.action = Utils.parseJSON('new-toga-url');
-
- let dateStr = new Date().toDateString();
- const fields = [
- ['account_name', `Payment Account made on ${dateStr}`],
- ['global', 'false'],
- ['type_code', 'CC'],
- ['tyler_info', authData.tyler_token],
- ['original_url', `${window.location.origin}/jurisdiction/${jurisdiction}/review/?payment_status=success`],
- ['error_url', `${window.location.origin}/jurisdiction/${jurisdiction}/review/?payment_status=failure`]
- ];
-
- fields.forEach(([name, value]) => {
- const input = document.createElement("input");
- input.type = "hidden";
- input.name = name;
- input.value = value;
- form.appendChild(input);
- });
-
- document.body.appendChild(form);
- form.submit();
- },
-
- handleCallback() {
- const status = Utils.getURLParam('payment_status');
-
- if (status === 'success') {
- Messages.showSuccess(gettext("Payment method added successfully!"));
- setTimeout(() => APIHandlers.loadPaymentAccounts(), 1000);
- Utils.cleanURL();
- } else if (status === 'failure') {
- Messages.showError(gettext("Failed to add payment method. Please try again."));
- Utils.cleanURL();
- }
- }
-};
-
// Navigation
const Navigation = {
goBack() {
- window.location.href = `/jurisdiction/${apiUtils.getCurrentJurisdiction()}/upload`;
+ window.location.href = `/jurisdiction/${apiUtils.getCurrentJurisdiction()}/payment`;
},
changeDocument(type) {
@@ -531,7 +433,7 @@ const FilingHandler = {
return;
}
- const selectedPaymentMethod = document.querySelector('input[name="paymentMethod"]:checked');
+ const selectedPaymentMethod = document.getElementById('paymentAccountID');
if (!selectedPaymentMethod) {
Messages.showError(gettext("Please select a payment method to continue."));
return;
@@ -541,15 +443,48 @@ const FilingHandler = {
Messages.hide();
try {
- const result = await this.processSubmission(userData, selectedPaymentMethod.value);
+ const result = await this.processSubmission(userData, selectedPaymentMethod.getAttribute("value"));
this.handleSubmissionResult(result);
} catch (error) {
console.log("Error on submission: %o", error)
- Messages.showError(gexttext("An unexpected error occurred. Please try again."));
+ Messages.showError(gettext("An unexpected error occurred. Please try again."));
this.setSubmissionState(false);
}
},
+ async queryFees() {
+ const userData = await this.collectUserData();
+ const selectedPaymentMethod = document.getElementById('paymentAccountID');
+
+ this.setFeesState(true);
+
+ try {
+ const result = await this.processFees(userData, selectedPaymentMethod.getAttribute("value"));
+ this.handleFeesResponse(result);
+ } catch (error) {
+ console.log("Error on submission: %o", error)
+ Messages.showError(gettext("An unexpected error occurred. Please try again."));
+ this.setFeesState(false);
+ }
+ },
+
+ async processFees(userData, paymentAccountID) {
+ let [caseData, uploadData] = await Promise.all([
+ apiUtils.getCaseData(),
+ apiUtils.getUploadData()
+ ]);
+
+ caseData = caseData.data.case_data;
+
+ const efilingData = this.buildEFilingData(userData, caseData, uploadData, paymentAccountID);
+
+ return await apiUtils.cachedPost(CONFIG.URLS.QUERY_FEES, {
+ efile_data: efilingData,
+ confirm_submission: true,
+ payment_account_id: paymentAccountID
+ });
+ },
+
collectUserData() {
return {
fullName: FieldManager.getFieldValue('userName'),
@@ -563,6 +498,14 @@ const FilingHandler = {
};
},
+ setFeesState(isQueryingFees) {
+ const submitButton = Utils.getElement('submitButton');
+ const loadingSpinner = Utils.getElement('loadingSpinner');
+
+ if (submitButton) submitButton.disabled = isQueryingFees;
+ if (loadingSpinner) loadingSpinner.style.display = isQueryingFees ? "block" : "none";
+ },
+
setSubmissionState(isSubmitting) {
const submitButton = Utils.getElement('submitButton');
const loadingSpinner = Utils.getElement('loadingSpinner');
@@ -573,8 +516,8 @@ const FilingHandler = {
async processSubmission(userData, paymentAccountID) {
let [caseData, uploadData] = await Promise.all([
- DataManager.fetchJSON(CONFIG.URLS.CASE_DATA),
- DataManager.fetchJSON(CONFIG.URLS.UPLOAD_DATA)
+ apiUtils.getCaseData(),
+ apiUtils.getUploadData()
]);
caseData = caseData.data.case_data;
@@ -792,6 +735,29 @@ const FilingHandler = {
Messages.showError(result?.error || "An error occurred during submission.");
this.setSubmissionState(false);
}
+ },
+
+ handleFeesResponse(result) {
+ if (result?.success) {
+ let htmlStr = `
+
Total: $${result.api_response.feesCalculationAmount.value}
+
+
+ `;
+ for (let specificFee of result.api_response.allowanceCharge) {
+ if (specificFee.chargeIndicator.value) {
+ htmlStr += `- ${specificFee.allowanceChargeReason.value}: $${specificFee.amount.value}
`;
+ }
+ }
+ htmlStr += "
";
+
+ let infoElem = document.getElementById("paymentInfo");
+ infoElem.innerHTML = htmlStr;
+ document.getElementById("paymentSection").removeAttribute("hidden");
+ } else {
+ Messages.showError(result?.error || "An error occurred when calculating fees.");
+ }
+ this.setFeesState(false);
}
};
@@ -799,7 +765,6 @@ const FilingHandler = {
const ReviewApp = {
async init() {
await this.loadAllData();
- PaymentHandler.handleCallback();
APIHandlers.fetchPartyType();
},
@@ -816,8 +781,8 @@ const ReviewApp = {
await Promise.all([
APIHandlers.loadUserInfo(),
APIHandlers.loadUploadData(),
- APIHandlers.loadPaymentAccounts()
]);
+ await APIHandlers.loadPaymentAccounts()
}
};
@@ -825,7 +790,7 @@ const ReviewApp = {
window.toggleEdit = FieldManager.toggleEdit.bind(FieldManager);
window.goBack = Navigation.goBack;
window.submitFiling = FilingHandler.submitFiling.bind(FilingHandler);
-window.PaymentHandler = PaymentHandler;
+window.queryFees = FilingHandler.queryFees.bind(FilingHandler);
window.Navigation = Navigation;
// Initialize app when DOM is ready
diff --git a/efile_app/efile/static/js/upload-handler.js b/efile_app/efile/static/js/upload-handler.js
index 11c4340..4973487 100644
--- a/efile_app/efile/static/js/upload-handler.js
+++ b/efile_app/efile/static/js/upload-handler.js
@@ -785,8 +785,8 @@ class UploadHandler {
await this.saveUploadDataToSession(uploadDataWithUrls);
- // Redirect to review page
- window.location.href = `/jurisdiction/${this.jurisdiction}/review/`;
+ // Redirect to payments page
+ window.location.href = `/jurisdiction/${this.jurisdiction}/payment/`;
} catch (error) {
console.error('Form submission error:', error);
diff --git a/efile_app/efile/templates/efile/payment.html b/efile_app/efile/templates/efile/payment.html
new file mode 100644
index 0000000..d4e3c47
--- /dev/null
+++ b/efile_app/efile/templates/efile/payment.html
@@ -0,0 +1,78 @@
+{% load static %}
+{% load i18n %}
+
+
+
+
+
+
Review Case Details
+
+
+
+
+
+
+ {% csrf_token %}
+ {% include "efile/components/profile_header.html" %}
+
+
+
{% translate "Payment information" %}
+
{% translate "Choose how you want to pay the filing fees to the court." %}
+
+
{% translate "Choose your payment method" %}
+
+
+
+
{% translate "Existing" %}
+
+
+
+
+
+
+
{% translate "Filing Fees" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading...
+
+
{% translate "Calculating cost of filing..." %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ case_data|json_script:"case-data" }}
+ {{ new_toga_url|json_script:"new-toga-url" }}
+
+
+
+
+
+
diff --git a/efile_app/efile/templates/efile/review.html b/efile_app/efile/templates/efile/review.html
index da1fcd7..2704050 100644
--- a/efile_app/efile/templates/efile/review.html
+++ b/efile_app/efile/templates/efile/review.html
@@ -179,13 +179,12 @@
{% translate "Your documents for filing" %}
{% translate "Payment method" %}
-
{% translate "Existing" %}
-
+
{% translate "Filing Fees" %}
diff --git a/efile_app/efile/templates/efile/upload.html b/efile_app/efile/templates/efile/upload.html
index a2b4dde..f1c257e 100644
--- a/efile_app/efile/templates/efile/upload.html
+++ b/efile_app/efile/templates/efile/upload.html
@@ -197,7 +197,7 @@
Before you continue
{% translate "Back to case information" %}
diff --git a/efile_app/efile/urls.py b/efile_app/efile/urls.py
index 1ca73fe..df2fa43 100644
--- a/efile_app/efile/urls.py
+++ b/efile_app/efile/urls.py
@@ -8,6 +8,7 @@
from .views.expert_form import efile_expert_form
from .views.login import efile_login, efile_logout, efile_password_reset
from .views.options import efile_options
+from .views.payment import efile_payment
from .views.register import efile_register
from .views.review import case_review
from .views.session_api import (
@@ -46,6 +47,7 @@ def jurisdiction_homepage(request, jurisdiction):
path("jurisdiction/
/expert_form/", efile_expert_form, name="expert_form"),
path("jurisdiction//upload_first/", efile_upload_first, name="upload_first"),
path("jurisdiction//upload/", efile_upload, name="upload"),
+ path("jurisdiction//payment/", efile_payment, name="payment"),
path("jurisdiction//review/", case_review, name="case_review"),
path("jurisdiction//filing-confirmation/", filing_confirmation, name="filing_confirmation"),
# Session API endpoints
diff --git a/efile_app/efile/utils/proxy_connection.py b/efile_app/efile/utils/proxy_connection.py
index 3bed940..44cee08 100644
--- a/efile_app/efile/utils/proxy_connection.py
+++ b/efile_app/efile/utils/proxy_connection.py
@@ -77,6 +77,7 @@ def get_party_type_code_from_api(court_code, case_type_code, jurisdiction, targe
def get_headers():
return {
"Accept": "application/json",
+ "Content-Type": "application/json",
"User-Agent": "LITEfile-Client/1.0",
"X-API-Key": getattr(settings, "SUFFOLK_EFILE_API_KEY", None),
}
diff --git a/efile_app/efile/views/payment.py b/efile_app/efile/views/payment.py
new file mode 100644
index 0000000..5c5ce68
--- /dev/null
+++ b/efile_app/efile/views/payment.py
@@ -0,0 +1,35 @@
+import logging
+
+from django.conf import settings
+from django.contrib import messages
+from django.shortcuts import redirect, render
+
+from ..utils.case_data_utils import get_case_data
+
+logger = logging.getLogger(__name__)
+
+
+def efile_payment(request, jurisdiction):
+ """Review view for case details before final submission."""
+ # Get case data from session
+ case_data = get_case_data(request)
+ logger.debug("Review view case_data %s", case_data)
+
+ # Add user email from session if available and not already in case_data
+ user_email = request.session.get("user_email")
+ if user_email and not case_data.get("email"):
+ case_data["email"] = user_email
+
+ # If no case data exists, redirect back to expert form
+ if not case_data:
+ messages.error(request, "Please complete the case details first.")
+ return redirect("expert_form", jurisdiction=jurisdiction)
+
+ new_toga_url = f"{settings.EFSP_URL}/jurisdictions/{jurisdiction}/payments/new-toga-account"
+
+ context = {
+ "new_toga_url": new_toga_url,
+ "case_data": case_data,
+ }
+
+ return render(request, "efile/payment.html", context)