|
- <%
- ' 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 = "<!doctype html><html><body style=""margin:0; padding:0; background:#faf7fd; font-family:Arial, Helvetica, sans-serif;"">" & _
- "<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);"">" & _
- "<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>" & _
- "</td></tr>" & _
- "<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>" & _
- "<h1 style=""color:#201a27; font-size:22px; margin:6px 0 4px; font-family:Arial, Helvetica, sans-serif;"">Thanks! We've received your order details.</h1>" & _
- "<p style=""color:#6d6574; font-size:13px; margin:0 0 8px;"">Jurisdiction number <strong style=""color:#201a27;"">" & H(order("JurisdictionNumber")) & "</strong>" & _
- " · submitted " & H(EmailDate(model.SubmittedAt)) & "</p>"
-
- 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 & "<p style=""color:#6d6574; font-size:12px; margin-top:24px;"">Questions about this order? Just reply to this email.</p>" & _
- "</td></tr>" & _
- "<tr><td style=""background:#f0e7f8; padding:16px 32px; text-align:center;"">" & _
- "<span style=""color:#6529a0; font-size:11px;"">Purple Envelope · Addressing & Barcoding</span>" & _
- "</td></tr>" & _
- "</table></td></tr></table></body></html>"
-
- BuildOrderDetailsEmailBody = html
- End Function
-
- Private Function EmailSection(sectionTitle, rowsHtml)
- If Len(rowsHtml) = 0 Then
- EmailSection = ""
- Else
- EmailSection = "<h2 style=""color:#4d1d78; font-size:14px; margin:24px 0 8px; padding-top:16px; border-top:1px solid #f0e7f8; font-family:Arial, Helvetica, sans-serif;"">" & _
- H(sectionTitle) & "</h2>" & _
- "<table role=""presentation"" width=""100%"" cellpadding=""0"" cellspacing=""0"">" & rowsHtml & "</table>"
- End If
- End Function
-
- Private Function EmailRow(label, value)
- If Len(Trim(value & "")) = 0 Then
- EmailRow = ""
- Else
- EmailRow = "<tr>" & _
- "<td style=""padding:6px 0; color:#6d6574; font-size:13px; width:45%; vertical-align:top;"">" & H(label) & "</td>" & _
- "<td style=""padding:6px 0; color:#201a27; font-size:13px; font-weight:600;"">" & H(value) & "</td>" & _
- "</tr>"
- 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
- %>
|