<% ' Auto-generated Controller: OrderApi ' Generated on 7/28/2026 2:56:29 PM ' Generator: generateController.vbs v1.0 ' ' Remember to: ' 1. Add to app/controllers/autoload_controllers.asp ' 2. Register in core/lib.ControllerRegistry.asp ' 3. Add routes in public/Default.asp Class OrderApiController_Class Private m_useLayout Private m_title Private Sub Class_Initialize() m_useLayout = False ' this controller returns JSON only, never the shared HTML layout m_title = "OrderApi" End Sub Public Property Get useLayout useLayout = m_useLayout End Property Public Property Let useLayout(v) m_useLayout = v End Property Public Property Get Title Title = m_title End Property Public Property Let Title(v) m_title = v End Property '------------------------------------------------------------------------------------------------------------------- ' POST: accept the completed SurveyJS order-details form (as JSON) for the order matching ?token= '------------------------------------------------------------------------------------------------------------------- Public Sub SubmitOrderDetails() Response.ContentType = "application/json" If Request.ServerVariables("REQUEST_METHOD") <> "POST" Then WriteJsonError "405 Method Not Allowed", "This endpoint only accepts POST." Exit Sub End If Dim token, order token = Trim(Request.QueryString("token")) Set order = Nothing If Len(token) > 0 Then Set order = OrdersRepository().FindByToken(token) If order Is Nothing Then WriteJsonError "404 Not Found", "Order not found." Exit Sub End If If order("TokenExpiresAt") < Now() Then WriteJsonError "410 Gone", "This order link has expired. Please request a new one." Exit Sub End If Dim csrfToken : csrfToken = Request.ServerVariables("HTTP_X_CSRF_TOKEN") If Not HTMLSecurity().IsValidAntiCSRFToken("Order.Continue", csrfToken) Then WriteJsonError "403 Forbidden", "Your form session expired. Please refresh the page and try again." Exit Sub End If Dim answers On Error Resume Next json().loadJSON GetRawJsonFromRequest() If Err.Number <> 0 Then Err.Clear On Error GoTo 0 WriteJsonError "400 Bad Request", "Could not read the submitted form data." Exit Sub End If On Error GoTo 0 Set answers = json().data Dim validationError : validationError = ValidateAnswers(answers) If Len(validationError) > 0 Then WriteJsonError "400 Bad Request", validationError Exit Sub End If Dim model : Set model = New POBO_OrderDetails On Error Resume Next MapOrderDetailsAnswers model, answers model.SubmittedAt = Now() If Err.Number <> 0 Then Err.Clear On Error GoTo 0 WriteJsonError "400 Bad Request", "The submitted data could not be processed. Please check your entries and try again." Exit Sub End If On Error GoTo 0 On Error Resume Next OrderDetailsRepository().SaveForOrder order("OrderID"), model If Err.Number <> 0 Then Err.Clear On Error GoTo 0 WriteJsonError "500 Internal Server Error", "Could not save your order details. Please try again." Exit Sub End If On Error GoTo 0 ' Best-effort: the order is already safely persisted at this point, so an SMTP hiccup ' should not turn a successful submission into a failed one. SendOrderDetailsEmail order, model Response.Write "{""success"":true}" End Sub '------------------------------------------------------------------------------------------------------------------- ' Order-confirmation email '------------------------------------------------------------------------------------------------------------------- Private Sub SendOrderDetailsEmail(order, model) On Error Resume Next Dim smtpPort : smtpPort = GetAppSetting("SmtpPort") If Not IsNumeric(smtpPort) Then smtpPort = 25 Dim mail : Set mail = CDOEmail() mail.SMTPServer = GetAppSetting("SmtpServer") mail.SMTPPort = CInt(smtpPort) mail.SMTPUsername = GetAppSetting("SmtpUsername") mail.SMTPPassword = GetAppSetting("SmtpPassword") mail.SMTPUseSSL = (LCase(GetAppSetting("SmtpUseSSL")) = "true") mail.From = GetAppSetting("SmtpFromAddress") mail.Subject = "Your Purple Envelope order details - Jurisdiction " & order("JurisdictionNumber") mail.IsBodyHTML = True mail.Body = BuildOrderDetailsEmailBody(order, model) mail.AddRecipient "To", order("Email") mail.Send Err.Clear On Error GoTo 0 End Sub Private Function BuildOrderDetailsEmailBody(order, model) Dim html html = "" & _ "
" & _ "" & _ "" & _ "" & _ "" & _ "
" & _ "PE" & _ "Purple Envelope" & _ "
Addressing & Barcoding
" & _ "
" & _ "
Order Received
" & _ "

Thanks! We've received your order details.

" & _ "

Jurisdiction number " & H(order("JurisdictionNumber")) & "" & _ " · submitted " & H(EmailDate(model.SubmittedAt)) & "

" html = html & EmailSection("Contact Information", _ EmailRow("Contact Name", model.ContactName) & _ EmailRow("Municipality", model.Municipality) & _ EmailRow("Phone", model.Phone)) If IsTrue(model.PurpleWantsQuote) Then html = html & EmailSection("Purple Ballot Envelopes", _ EmailRow("Print option", LabelPrintOption(model.PurplePrintOption)) & _ EmailRow("Envelope stock", LabelEnvelopeStock(model.PurpleEnvelopeStock)) & _ EmailRow("Envelope provider", model.PurpleEnvelopeProvider) & _ EmailRow("Also wants KCI blue envelopes", EmailYesNo(model.PurpleWantsBlueEnvelopes)) & _ EmailRow("Print style", LabelPrintStyle(model.PurplePrintStyle)) & _ EmailRow("Color coding by", LabelColorCodingBy(model.PurpleColorCodingBy)) & _ EmailRow("Number of precincts", model.PurplePrecinctCount) & _ EmailRow("Track with TrackMI Ballot", EmailYesNo(model.PurpleTrackMIBallot)) & _ EmailRow("Extra envelopes requested", model.PurpleExtraEnvelopeQty) & _ EmailRow("Requested delivery date", EmailDate(model.PurpleDeliveryDate))) End If If IsTrue(model.BlueWantsQuote) Then html = html & EmailSection("Blue AV Envelopes", _ EmailRow("Provider", LabelBlueProvider(model.BlueEnvelopeProvider)) & _ EmailRow("Print permit", EmailYesNo(model.BluePrintPermit)) & _ EmailRow("Permit number", model.BluePermitNumber) & _ EmailRow("City of permit holder", model.BluePermitCity) & _ EmailRow("Permit class", model.BluePermitClass) & _ EmailRow("Print return address", EmailYesNo(model.BluePrintReturnAddress)) & _ EmailRow("Requested delivery date", EmailDate(model.BlueDeliveryDate))) End If If IsTrue(model.MailingWantsService) Then html = html & EmailSection("Ballot Mailing Service", _ EmailRow("Postage", LabelPostageOption(model.MailingPostageOption)) & _ EmailRow("Nonprofit status with USPS", LabelNonprofitStatus(model.MailingHasNonprofitStatus)) & _ EmailRow("Sort ballots", EmailYesNo(model.MailingWantsSorting)) & _ EmailRow("Preferred pickup date", EmailDate(model.MailingPickupDate))) End If If NumOrZero(model.SecrecySleevesQty) > 0 Or NumOrZero(model.IVotedStickerRolls) > 0 Or NumOrZero(model.FutureVoterStickerRolls) > 0 Then html = html & EmailSection("Additional Election Items", _ EmailRow("Secrecy sleeves (quantity)", model.SecrecySleevesQty) & _ EmailRow("""I Voted"" stickers (rolls of 250)", model.IVotedStickerRolls) & _ EmailRow("""Future Voter"" stickers (rolls of 500)", model.FutureVoterStickerRolls)) End If html = html & "

Questions about this order? Just reply to this email.

" & _ "
" & _ "Purple Envelope · Addressing & Barcoding" & _ "
" BuildOrderDetailsEmailBody = html End Function Private Function EmailSection(sectionTitle, rowsHtml) If Len(rowsHtml) = 0 Then EmailSection = "" Else EmailSection = "

" & _ H(sectionTitle) & "

" & _ "" & rowsHtml & "
" End If End Function Private Function EmailRow(label, value) If Len(Trim(value & "")) = 0 Then EmailRow = "" Else EmailRow = "" & _ "" & H(label) & "" & _ "" & H(value) & "" & _ "" End If End Function ' Null-safe boolean check for gating whole sections - a section's top-level "do you want ' this?" question isn't marked required client-side, so it may be missing (Null) rather ' than explicitly false. Private Function IsTrue(v) If IsNull(v) Then IsTrue = False Else IsTrue = CBool(v) End If End Function Private Function EmailYesNo(v) If IsNull(v) Then EmailYesNo = "" ElseIf CBool(v) Then EmailYesNo = "Yes" Else EmailYesNo = "No" End If End Function Private Function EmailDate(v) If IsNull(v) Then EmailDate = "" Else On Error Resume Next EmailDate = MonthName(Month(v)) & " " & Day(v) & ", " & Year(v) If Err.Number <> 0 Then EmailDate = "" Err.Clear On Error GoTo 0 End If End Function Private Function NumOrZero(v) If IsNull(v) Or Not IsNumeric(v) Then NumOrZero = 0 Else NumOrZero = CDbl(v) End If End Function ' Human-readable labels for the coded choice values written by continue.asp's SurveyJS ' model - falls back to the raw code for anything unrecognized rather than hiding it. Private Function LabelPrintOption(code) Select Case CStr(code & "") Case "AddressesAndPostage" : LabelPrintOption = "Addresses and return postage" Case "PostageOnly" : LabelPrintOption = "Return postage only" Case Else : LabelPrintOption = CStr(code & "") End Select End Function Private Function LabelEnvelopeStock(code) Select Case CStr(code & "") Case "KCIStock" : LabelEnvelopeStock = "KCI's stock" Case "OwnStock" : LabelEnvelopeStock = "My own stock" Case Else : LabelEnvelopeStock = CStr(code & "") End Select End Function Private Function LabelPrintStyle(code) Select Case CStr(code & "") Case "ColorCoding" : LabelPrintStyle = "Color Coding" Case "BlackOnly" : LabelPrintStyle = "Black Only" Case Else : LabelPrintStyle = CStr(code & "") End Select End Function Private Function LabelColorCodingBy(code) Select Case CStr(code & "") Case "Precinct" : LabelColorCodingBy = "Precinct" Case "Election" : LabelColorCodingBy = "Election" Case Else : LabelColorCodingBy = CStr(code & "") End Select End Function Private Function LabelBlueProvider(code) Select Case CStr(code & "") Case "KCI" : LabelBlueProvider = "KCI" Case "OtherProvider" : LabelBlueProvider = "Other provider" Case Else : LabelBlueProvider = CStr(code & "") End Select End Function Private Function LabelPostageOption(code) Select Case CStr(code & "") Case "FirstClass" : LabelPostageOption = "Yes, at First Class Rate" Case "NonprofitRate" : LabelPostageOption = "Yes, at Nonprofit Rate" Case "No" : LabelPostageOption = "No" Case Else : LabelPostageOption = CStr(code & "") End Select End Function Private Function LabelNonprofitStatus(code) Select Case CStr(code & "") Case "Yes" : LabelNonprofitStatus = "Yes" Case "No" : LabelNonprofitStatus = "No" Case "NotSure" : LabelNonprofitStatus = "I'm not sure" Case Else : LabelNonprofitStatus = CStr(code & "") End Select End Function '------------------------------------------------------------------------------------------------------------------- ' Re-validates the submission server-side, mirroring the SurveyJS required/enum rules in ' continue.asp - the client-side checks are a UX convenience, not something the server can ' trust. Returns "" if valid, or a "; "-joined list of validation error messages. '------------------------------------------------------------------------------------------------------------------- Private Function ValidateAnswers(answers) Dim errors() : ReDim errors(-1) RequireNonEmptyString errors, answers, "ContactName", "Contact name is required." RequireNonEmptyString errors, answers, "Municipality", "Municipality is required." RequireNonEmptyString errors, answers, "Phone", "Phone number is required." RequireEnum errors, answers, "PurplePrintOption", Array("AddressesAndPostage", "PostageOnly") RequireEnum errors, answers, "PurpleEnvelopeStock", Array("KCIStock", "OwnStock") RequireEnum errors, answers, "PurplePrintStyle", Array("ColorCoding", "BlackOnly") RequireEnum errors, answers, "PurpleColorCodingBy", Array("Precinct", "Election") RequireEnum errors, answers, "BlueEnvelopeProvider", Array("KCI", "OtherProvider") RequireEnum errors, answers, "BluePermitClass", Array("NP", "STD") RequireEnum errors, answers, "MailingPostageOption", Array("FirstClass", "NonprofitRate", "No") RequireEnum errors, answers, "MailingHasNonprofitStatus", Array("Yes", "No", "NotSure") RequireBoolean errors, answers, "PurpleWantsQuote" RequireBoolean errors, answers, "PurpleWantsBlueEnvelopes" RequireBoolean errors, answers, "PurpleTrackMIBallot" RequireBoolean errors, answers, "BlueWantsQuote" RequireBoolean errors, answers, "BluePrintPermit" RequireBoolean errors, answers, "BluePrintReturnAddress" RequireBoolean errors, answers, "MailingWantsService" RequireBoolean errors, answers, "MailingWantsSorting" RequireNonNegativeInteger errors, answers, "PurplePrecinctCount" RequireNonNegativeInteger errors, answers, "PurpleExtraEnvelopeQty" RequireNonNegativeInteger errors, answers, "SecrecySleevesQty" RequireNonNegativeInteger errors, answers, "IVotedStickerRolls" RequireNonNegativeInteger errors, answers, "FutureVoterStickerRolls" RequireValidDate errors, answers, "PurpleDeliveryDate" RequireValidDate errors, answers, "BlueDeliveryDate" RequireValidDate errors, answers, "MailingPickupDate" If UBound(errors) >= 0 Then ValidateAnswers = Join(errors, "; ") Else ValidateAnswers = "" End If End Function Private Sub AddValidationError(ByRef errors, msg) ReDim Preserve errors(UBound(errors) + 1) errors(UBound(errors)) = msg End Sub ' Required on the client (page 1) - re-checked here since the client can't be trusted. Private Sub RequireNonEmptyString(ByRef errors, answers, key, msg) Dim v : v = AnswerValue(answers, key) If IsNull(v) Or Len(Trim(v & "")) = 0 Then AddValidationError errors, msg End Sub ' Only checked when present - these questions are conditionally hidden by visibleIf, so a ' missing value just means the question wasn't shown, not an invalid submission. Private Sub RequireEnum(ByRef errors, answers, key, allowedValues) Dim v : v = AnswerValue(answers, key) If Not IsNull(v) Then Dim found : found = False Dim i For i = 0 To UBound(allowedValues) If CStr(v) = allowedValues(i) Then found = True Next If Not found Then AddValidationError errors, key & " has an invalid value." End If End Sub Private Sub RequireBoolean(ByRef errors, answers, key) Dim v : v = AnswerValue(answers, key) If Not IsNull(v) Then If TypeName(v) <> "Boolean" Then AddValidationError errors, key & " must be true or false." End If End Sub Private Sub RequireNonNegativeInteger(ByRef errors, answers, key) Dim v : v = AnswerValue(answers, key) If Not IsNull(v) Then If Not IsNumeric(v) Then AddValidationError errors, key & " must be a number." ElseIf CDbl(v) < 0 Then AddValidationError errors, key & " cannot be negative." ElseIf CDbl(v) <> Int(CDbl(v)) Then AddValidationError errors, key & " must be a whole number." End If End If End Sub Private Sub RequireValidDate(ByRef errors, answers, key) Dim v : v = AnswerValue(answers, key) If Not IsNull(v) Then If Len(Trim(v & "")) > 0 And Not IsDate(v) Then AddValidationError errors, key & " is not a valid date." End If End If End Sub '------------------------------------------------------------------------------------------------------------------- ' Private helpers '------------------------------------------------------------------------------------------------------------------- Private Sub MapOrderDetailsAnswers(ByRef model, answers) ' Contact model.ContactName = AnswerValue(answers, "ContactName") model.Municipality = AnswerValue(answers, "Municipality") model.Phone = AnswerValue(answers, "Phone") ' Purple envelope model.PurpleWantsQuote = AnswerValue(answers, "PurpleWantsQuote") model.PurplePrintOption = AnswerValue(answers, "PurplePrintOption") model.PurpleEnvelopeStock = AnswerValue(answers, "PurpleEnvelopeStock") model.PurpleEnvelopeProvider = AnswerValue(answers, "PurpleEnvelopeProvider") model.PurpleWantsBlueEnvelopes = AnswerValue(answers, "PurpleWantsBlueEnvelopes") model.PurplePrintStyle = AnswerValue(answers, "PurplePrintStyle") model.PurpleColorCodingBy = AnswerValue(answers, "PurpleColorCodingBy") model.PurplePrecinctCount = AnswerValue(answers, "PurplePrecinctCount") model.PurpleTrackMIBallot = AnswerValue(answers, "PurpleTrackMIBallot") model.PurpleExtraEnvelopeQty = AnswerValue(answers, "PurpleExtraEnvelopeQty") model.PurpleDeliveryDate = AnswerValue(answers, "PurpleDeliveryDate") ' Blue envelope model.BlueWantsQuote = AnswerValue(answers, "BlueWantsQuote") model.BlueEnvelopeProvider = AnswerValue(answers, "BlueEnvelopeProvider") model.BluePrintPermit = AnswerValue(answers, "BluePrintPermit") model.BluePermitNumber = AnswerValue(answers, "BluePermitNumber") model.BluePermitCity = AnswerValue(answers, "BluePermitCity") model.BluePermitClass = AnswerValue(answers, "BluePermitClass") model.BluePrintReturnAddress = AnswerValue(answers, "BluePrintReturnAddress") model.BlueDeliveryDate = AnswerValue(answers, "BlueDeliveryDate") ' Mailing service model.MailingWantsService = AnswerValue(answers, "MailingWantsService") model.MailingPostageOption = AnswerValue(answers, "MailingPostageOption") model.MailingHasNonprofitStatus = AnswerValue(answers, "MailingHasNonprofitStatus") model.MailingWantsSorting = AnswerValue(answers, "MailingWantsSorting") model.MailingPickupDate = AnswerValue(answers, "MailingPickupDate") ' Additional items model.SecrecySleevesQty = AnswerValue(answers, "SecrecySleevesQty") model.IVotedStickerRolls = AnswerValue(answers, "IVotedStickerRolls") model.FutureVoterStickerRolls = AnswerValue(answers, "FutureVoterStickerRolls") End Sub ' Returns the value for key in a parsed aspJSON Dictionary, or Null if the question was ' skipped (hidden by visibleIf logic and so never included in SurveyJS's result data). Private Function AnswerValue(answers, key) If answers.Exists(key) Then AnswerValue = answers.Item(key) Else AnswerValue = Null End If End Function Private Sub WriteJsonError(statusLine, message) Response.Status = statusLine Response.Write "{""success"":false,""error"":" & JsonEncodeString(message) & "}" End Sub Private Function JsonEncodeString(s) Dim out : out = s out = Replace(out, "\", "\\") out = Replace(out, """", "\""") out = Replace(out, Chr(10), "\n") out = Replace(out, Chr(13), "\r") JsonEncodeString = """" & out & """" End Function End Class ' Singleton instance Dim OrderApiController_Class__Singleton Function OrderApiController() If IsEmpty(OrderApiController_Class__Singleton) Then Set OrderApiController_Class__Singleton = New OrderApiController_Class End If Set OrderApiController = OrderApiController_Class__Singleton End Function %>