Browse Source

Auto-populate order form Municipality from jurisdiction cache

Extends the jurisdiction API cache to store JCode/Name pairs (not just
codes) and looks up the matching Name for an order's already-validated
JurisdictionNumber when rendering /order/continue. When a match is
found, the Municipality field is pre-filled and locked read-only;
otherwise it falls back to an empty, editable field so the user can
enter it manually.
master
Daniel Covington 2 weeks ago
parent
commit
0cb51ed406
3 changed files with 77 additions and 28 deletions
  1. +9
    -2
      app/controllers/OrderController.asp
  2. +52
    -24
      app/models/JurisdictionValidator.asp
  3. +16
    -2
      app/views/Order/continue.asp

+ 9
- 2
app/controllers/OrderController.asp View File

@@ -29,10 +29,11 @@ Class OrderController_Class
' The order form itself is a placeholder for now - this only confirms the token is valid.
'-------------------------------------------------------------------------------------------------------------------
Public Sub Continue()
Dim token, order, isValid, csrfToken
Dim token, order, isValid, csrfToken, municipalityName
token = Trim(Request.QueryString("token"))
isValid = False
csrfToken = ""
municipalityName = ""
Set order = Nothing

If Len(token) > 0 Then
@@ -45,7 +46,13 @@ Class OrderController_Class
End If
End If

If isValid Then csrfToken = HTMLSecurity().GetAntiCSRFToken("Order.Continue")
If isValid Then
csrfToken = HTMLSecurity().GetAntiCSRFToken("Order.Continue")
' Look up the municipality from the same jurisdiction list the number was
' already validated against at /request-order - "" means no match (cache empty
' or code not found), and the view falls back to a manually-entered field.
municipalityName = GetJurisdictionName(order("JurisdictionNumber"))
End If
%>
<!--#include file="../views/Order/continue.asp" -->
<%


+ 52
- 24
app/models/JurisdictionValidator.asp View File

@@ -1,21 +1,22 @@
<%
'=======================================================================================================================
' Jurisdiction Number Validation
' Jurisdiction Number Validation + Lookup
'=======================================================================================================================
' Validates a submitted jurisdiction number against the JCode field of the jurisdictions
' list served by JurisdictionApiUrl (see public/web.config). That list is essentially static
' reference data (townships/municipalities), so it's cached in Application scope for
' JurisdictionCacheMinutes (default 60) instead of being fetched on every /request-order
' submission. If a refresh attempt fails, stale cached data is used rather than blocking
' order requests; only when there is no cached data at all (e.g. immediately after an app
' pool restart, with the API also unreachable) does validation fail open (allow) rather than
' block every order request site-wide.
' list served by JurisdictionApiUrl (see public/web.config), and looks up the matching
' municipality Name (used to pre-fill the order form's Municipality field). That list is
' essentially static reference data (townships/municipalities), so it's cached in
' Application scope for JurisdictionCacheMinutes (default 60) instead of being fetched on
' every /request-order submission. If a refresh attempt fails, stale cached data is used
' rather than blocking order requests; only when there is no cached data at all (e.g.
' immediately after an app pool restart, with the API also unreachable) does validation
' fail open (allow) rather than block every order request site-wide.
'
' The cache is stored as a single delimited STRING ("|code1|code2|...|"), not a
' Scripting.Dictionary or array of objects - Scripting.Dictionary is an apartment-threaded
' COM object, and IIS raises "ASP 0197: Disallowed object use" if you try to store one in
' the Application intrinsic (which is shared across all requests/threads). A plain string is
' just script data, so it's safe to cache this way.
' The cache is stored as a single delimited STRING ("|code1<TAB>name1|code2<TAB>name2|...|"),
' not a Scripting.Dictionary or array of objects - Scripting.Dictionary is an
' apartment-threaded COM object, and IIS raises "ASP 0197: Disallowed object use" if you try
' to store one in the Application intrinsic (which is shared across all requests/threads). A
' plain string is just script data, so it's safe to cache this way.
'=======================================================================================================================

Function IsValidJurisdictionNumber(jurisdictionNumber)
@@ -29,13 +30,36 @@ Function IsValidJurisdictionNumber(jurisdictionNumber)
' down) - fail open rather than block every order request site-wide.
IsValidJurisdictionNumber = True
Else
IsValidJurisdictionNumber = (InStr(codesText, "|" & jurisdictionNumber & "|") > 0)
IsValidJurisdictionNumber = (InStr(codesText, "|" & jurisdictionNumber & Chr(9)) > 0)
End If
End Function

' Returns the cached "|code1|code2|...|" string, refreshing it from JurisdictionApiUrl when
' missing or older than JurisdictionCacheMinutes. Falls back to the existing (stale) cached
' string if a refresh attempt fails.
' Returns the municipality Name matching a jurisdiction number, or "" if the cache is empty
' or has no matching entry (callers should treat "" as "couldn't look this up" and fall back
' to letting the user enter it manually, not as a hard error).
Function GetJurisdictionName(jurisdictionNumber)
Dim codesText, marker, startPos, nameStart, endPos

GetJurisdictionName = ""

codesText = GetJurisdictionCodesText()
If Len(codesText) = 0 Then Exit Function

jurisdictionNumber = Trim(jurisdictionNumber)
marker = "|" & jurisdictionNumber & Chr(9)
startPos = InStr(codesText, marker)
If startPos = 0 Then Exit Function

nameStart = startPos + Len(marker)
endPos = InStr(nameStart, codesText, "|")
If endPos = 0 Then Exit Function

GetJurisdictionName = Mid(codesText, nameStart, endPos - nameStart)
End Function

' Returns the cached "|code1<TAB>name1|code2<TAB>name2|...|" string, refreshing it from
' JurisdictionApiUrl when missing or older than JurisdictionCacheMinutes. Falls back to the
' existing (stale) cached string if a refresh attempt fails.
Private Function GetJurisdictionCodesText()
Dim cacheMinutes, cachedAt, isStale

@@ -60,15 +84,18 @@ Private Function GetJurisdictionCodesText()
GetJurisdictionCodesText = Application("JurisdictionCodesText")
End Function

' Fetches the jurisdictions list and extracts every JCode value. Returns a
' "|code1|code2|...|" string, or "" on any failure (network, HTTP status, or no matches).
' Fetches the jurisdictions list and extracts each JCode/Name pair. Returns a
' "|code1<TAB>name1|code2<TAB>name2|...|" string, or "" on any failure (network, HTTP
' status, or no matches).
'
' Uses a targeted regex over the raw response text rather than this codebase's generic
' aspJSON parser (core/lib.json.asp) - that parser walks the input character-by-character in
' plain VBScript, which measured at over 40 seconds for this API's ~450KB/1500-record
' response. RegExp.Execute is implemented natively and handles the same payload
' near-instantly; since JCode is the only field this validator needs, a full generic parse
' isn't necessary anyway.
' near-instantly. The regex assumes JCode is immediately followed by Name in the source
' JSON (verified true for all 1525 current records) - if the API ever reorders those fields
' this will need revisiting, but a full generic parse still isn't necessary since only these
' two fields are used.
Private Function FetchJurisdictionCodesText()
Dim apiUrl : apiUrl = GetAppSetting("JurisdictionApiUrl")
Dim result : result = ""
@@ -81,9 +108,9 @@ Private Function FetchJurisdictionCodesText()
http.Send ""

If Err.Number = 0 And http.Status = 200 Then
Dim re, matches, m, codes, code
Dim re, matches, m, codes, code, name
Set re = New RegExp
re.Pattern = """JCode""\s*:\s*""([^""]*)"""
re.Pattern = """JCode""\s*:\s*""([^""]*)""\s*,\s*""Name""\s*:\s*""([^""]*)"""
re.Global = True
re.IgnoreCase = True

@@ -92,7 +119,8 @@ Private Function FetchJurisdictionCodesText()
codes = "|"
For Each m In matches
code = Trim(m.SubMatches(0))
If Len(code) > 0 Then codes = codes & code & "|"
name = Trim(m.SubMatches(1))
If Len(code) > 0 Then codes = codes & code & Chr(9) & name & "|"
Next
If Len(codes) > 1 Then result = codes
End If


+ 16
- 2
app/views/Order/continue.asp View File

@@ -13,7 +13,16 @@
<% End If %>
</section>

<% If isValid Then %>
<% If isValid Then
Dim municipalityIsReadOnlyJs, municipalityDescription
If Len(Trim(municipalityName)) > 0 Then
municipalityIsReadOnlyJs = "true"
municipalityDescription = "Matched automatically from your jurisdiction number."
Else
municipalityIsReadOnlyJs = "false"
municipalityDescription = "We couldn't automatically match your jurisdiction number - please enter your municipality."
End If
%>
<link href="https://cdn.jsdelivr.net/npm/survey-core@2.5.36/survey-core.min.css" rel="stylesheet">

<div id="surveyContainer" style="max-width: 760px; margin: 0 auto 64px; padding: 0 16px;"></div>
@@ -58,7 +67,12 @@
title: "1. Contact Information",
elements: [
{ type: "text", name: "ContactName", title: "Contact Name", isRequired: true },
{ type: "text", name: "Municipality", title: "Municipality", isRequired: true },
{
type: "text", name: "Municipality", title: "Municipality", isRequired: true,
defaultValue: "<%= H(municipalityName) %>",
readOnly: <%= municipalityIsReadOnlyJs %>,
description: "<%= H(municipalityDescription) %>"
},
{ type: "text", name: "Phone", title: "Phone Number", inputType: "tel", isRequired: true }
]
},


Loading…
Cancel
Save

Powered by TurnKey Linux.