- Blue AV Envelope: City of permit holder and Permit Number are now always collected for an own-organization permit, regardless of nonprofit status - a permit lookup using only the nonprofit authorization code doesn't work, so the code is now additional info when nonprofit status is Yes, not a replacement for city/permit number. - Order confirmation email header simplified to "KCI Purple Envelope Order Confirmation" text, dropping the site's PE logo mark and tagline. - Replaced the IsValidJurisdictionNumber stub (previously always returned true) with real validation against JurisdictionApiUrl, cached in Application scope (JurisdictionCacheMinutes, default 60) since the jurisdiction list is essentially static reference data. Falls back to stale cached data on a refresh failure rather than blocking order requests; fails open only when there's no cache at all. Hit two Classic ASP gotchas along the way: Scripting.Dictionary can't be stored in Application scope (ASP 0197, apartment-threaded COM object) - cached a delimited string instead - and the codebase's generic aspJSON parser took 43s to parse the ~1500-record API response, so switched to a targeted regex extraction of just the JCode field (~1s).master
| @@ -146,9 +146,7 @@ Class OrderApiController_Class | |||||
| "<table role=""presentation"" width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""background:#faf7fd; padding:24px 0;""><tr><td align=""center"">" & _ | "<table role=""presentation"" width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""background:#faf7fd; padding:24px 0;""><tr><td align=""center"">" & _ | ||||
| "<table role=""presentation"" width=""600"" cellpadding=""0"" cellspacing=""0"" style=""background:#ffffff; border-radius:12px; overflow:hidden; box-shadow:0 4px 16px rgba(36,16,58,0.08);"">" & _ | "<table role=""presentation"" width=""600"" cellpadding=""0"" cellspacing=""0"" style=""background:#ffffff; border-radius:12px; overflow:hidden; box-shadow:0 4px 16px rgba(36,16,58,0.08);"">" & _ | ||||
| "<tr><td style=""background:#24103a; padding:24px 32px;"">" & _ | "<tr><td style=""background:#24103a; padding:24px 32px;"">" & _ | ||||
| "<span style=""display:inline-block; width:34px; height:34px; border-radius:50%; background:#7c3bc0; color:#fff; font-weight:800; font-size:12px; text-align:center; line-height:34px; vertical-align:middle;"">PE</span>" & _ | |||||
| "<span style=""color:#ffffff; font-weight:700; font-size:16px; margin-left:10px; vertical-align:middle;"">Purple Envelope</span>" & _ | |||||
| "<div style=""color:#dcc9ef; font-size:11px; margin-top:2px;"">Addressing & Barcoding</div>" & _ | |||||
| "<span style=""color:#ffffff; font-weight:700; font-size:18px;"">KCI Purple Envelope Order Confirmation</span>" & _ | |||||
| "</td></tr>" & _ | "</td></tr>" & _ | ||||
| "<tr><td style=""padding:32px;"">" & _ | "<tr><td style=""padding:32px;"">" & _ | ||||
| "<div style=""color:#7c3bc0; font-weight:800; font-size:11px; letter-spacing:0.06em; text-transform:uppercase;"">Order Received</div>" & _ | "<div style=""color:#7c3bc0; font-weight:800; font-size:11px; letter-spacing:0.06em; text-transform:uppercase;"">Order Received</div>" & _ | ||||
| @@ -182,14 +180,17 @@ Class OrderApiController_Class | |||||
| End If | End If | ||||
| If IsTrue(model.BlueWantsQuote) Then | If IsTrue(model.BlueWantsQuote) Then | ||||
| ' City/permit number are always collected for an own-organization permit - a | |||||
| ' permit lookup can't be done from the nonprofit authorization code alone, so | |||||
| ' that code is additional information when nonprofit status is Yes, not a | |||||
| ' replacement for city/permit number. | |||||
| Dim bluePermitLine | Dim bluePermitLine | ||||
| bluePermitLine = "" | bluePermitLine = "" | ||||
| If LabelPermitOwnership(model.BluePermitOwnership) = "My organization's permit" Then | If LabelPermitOwnership(model.BluePermitOwnership) = "My organization's permit" Then | ||||
| bluePermitLine = EmailRow("City of permit holder", model.BluePermitCity) & _ | |||||
| EmailRow("Permit number", model.BluePermitNumber) | |||||
| If IsTrue(model.BlueHasNonprofitStatus) Then | If IsTrue(model.BlueHasNonprofitStatus) Then | ||||
| bluePermitLine = EmailRow("Nonprofit authorization code", model.BlueNonprofitAuthCode) | |||||
| Else | |||||
| bluePermitLine = EmailRow("City of permit holder", model.BluePermitCity) & _ | |||||
| EmailRow("Permit number", model.BluePermitNumber) | |||||
| bluePermitLine = bluePermitLine & EmailRow("Nonprofit authorization code", model.BlueNonprofitAuthCode) | |||||
| End If | End If | ||||
| End If | End If | ||||
| @@ -2,9 +2,105 @@ | |||||
| '======================================================================================================================= | '======================================================================================================================= | ||||
| ' Jurisdiction Number Validation | ' Jurisdiction Number Validation | ||||
| '======================================================================================================================= | '======================================================================================================================= | ||||
| ' STUB: always returns True. Replace with a real lookup once a jurisdictions | |||||
| ' table/reference list exists to validate the submitted number against. | |||||
| ' 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. | |||||
| ' | |||||
| ' 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. | |||||
| '======================================================================================================================= | |||||
| Function IsValidJurisdictionNumber(jurisdictionNumber) | Function IsValidJurisdictionNumber(jurisdictionNumber) | ||||
| IsValidJurisdictionNumber = True | |||||
| Dim codesText | |||||
| codesText = GetJurisdictionCodesText() | |||||
| jurisdictionNumber = Trim(jurisdictionNumber) | |||||
| If Len(codesText) = 0 Then | |||||
| ' No jurisdiction list available at all (first request after startup, API also | |||||
| ' down) - fail open rather than block every order request site-wide. | |||||
| IsValidJurisdictionNumber = True | |||||
| Else | |||||
| IsValidJurisdictionNumber = (InStr(codesText, "|" & jurisdictionNumber & "|") > 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. | |||||
| Private Function GetJurisdictionCodesText() | |||||
| Dim cacheMinutes, cachedAt, isStale | |||||
| cacheMinutes = GetAppSetting("JurisdictionCacheMinutes") | |||||
| If Not IsNumeric(cacheMinutes) Then cacheMinutes = 60 | |||||
| cachedAt = Application("JurisdictionCodesFetchedAt") | |||||
| isStale = IsEmpty(cachedAt) | |||||
| If Not isStale Then isStale = (DateDiff("n", cachedAt, Now()) >= CInt(cacheMinutes)) | |||||
| If isStale Then | |||||
| Dim fresh | |||||
| fresh = FetchJurisdictionCodesText() | |||||
| If Len(fresh) > 0 Then | |||||
| Application.Lock | |||||
| Application("JurisdictionCodesText") = fresh | |||||
| Application("JurisdictionCodesFetchedAt") = Now() | |||||
| Application.Unlock | |||||
| End If | |||||
| End If | |||||
| 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). | |||||
| ' | |||||
| ' 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. | |||||
| Private Function FetchJurisdictionCodesText() | |||||
| Dim apiUrl : apiUrl = GetAppSetting("JurisdictionApiUrl") | |||||
| Dim result : result = "" | |||||
| On Error Resume Next | |||||
| Dim http : Set http = Server.CreateObject("Msxml2.ServerXMLHTTP") | |||||
| http.setTimeouts 5000, 5000, 5000, 5000 | |||||
| http.Open "GET", apiUrl, False | |||||
| http.Send "" | |||||
| If Err.Number = 0 And http.Status = 200 Then | |||||
| Dim re, matches, m, codes, code | |||||
| Set re = New RegExp | |||||
| re.Pattern = """JCode""\s*:\s*""([^""]*)""" | |||||
| re.Global = True | |||||
| re.IgnoreCase = True | |||||
| Set matches = re.Execute(http.responseText) | |||||
| If Err.Number = 0 And matches.Count > 0 Then | |||||
| codes = "|" | |||||
| For Each m In matches | |||||
| code = Trim(m.SubMatches(0)) | |||||
| If Len(code) > 0 Then codes = codes & code & "|" | |||||
| Next | |||||
| If Len(codes) > 1 Then result = codes | |||||
| End If | |||||
| End If | |||||
| Err.Clear | |||||
| On Error GoTo 0 | |||||
| FetchJurisdictionCodesText = result | |||||
| End Function | End Function | ||||
| %> | %> | ||||
| @@ -217,13 +217,13 @@ | |||||
| }, | }, | ||||
| { | { | ||||
| type: "text", name: "BluePermitCity", title: "City of permit holder", | type: "text", name: "BluePermitCity", title: "City of permit holder", | ||||
| description: "Please provide the following information.", | |||||
| visibleIf: "{BlueHasNonprofitStatus} = false", | |||||
| description: "A permit lookup can't be done from the authorization code alone, so please also provide the following information.", | |||||
| visibleIf: "{BluePermitOwnership} = 'OwnPermit'", | |||||
| isRequired: true | isRequired: true | ||||
| }, | }, | ||||
| { | { | ||||
| type: "text", name: "BluePermitNumber", title: "Permit Number", | type: "text", name: "BluePermitNumber", title: "Permit Number", | ||||
| visibleIf: "{BlueHasNonprofitStatus} = false", | |||||
| visibleIf: "{BluePermitOwnership} = 'OwnPermit'", | |||||
| isRequired: true | isRequired: true | ||||
| } | } | ||||
| ] | ] | ||||
| @@ -53,6 +53,14 @@ | |||||
| <!-- Hours before an order continuation link (token) expires --> | <!-- Hours before an order continuation link (token) expires --> | ||||
| <add key="OrderTokenExpirationHours" value="24" /> | <add key="OrderTokenExpirationHours" value="24" /> | ||||
| <!-- | |||||
| Jurisdiction number validation (request-order form). The list is essentially static | |||||
| reference data, so it's cached in Application scope rather than fetched on every | |||||
| submission - see JurisdictionCacheMinutes. | |||||
| --> | |||||
| <add key="JurisdictionApiUrl" value="http://192.168.1.40:8081/api/jurisdictions" /> | |||||
| <add key="JurisdictionCacheMinutes" value="60" /> | |||||
| </appSettings> | </appSettings> | ||||
| <system.webServer> | <system.webServer> | ||||
Powered by TurnKey Linux.