Extract email sending into OrderMailer, answer-to-model mapping into OrderDetailsAnswersMapper, label mapping into OrderDetailsLabels, and generic dictionary-based field validation into core/lib.Validations.asp (alongside a small lib.JsonResponse helper), replacing the private helpers previously duplicated inline in OrderApiController and RequestOrderController.master
| @@ -78,7 +78,7 @@ Class OrderApiController_Class | |||
| On Error GoTo 0 | |||
| Set answers = json().data | |||
| Dim validationError : validationError = ValidateAnswers(answers) | |||
| Dim validationError : validationError = ValidateOrderDetailsAnswers(answers) | |||
| If Len(validationError) > 0 Then | |||
| WriteJsonError "400 Bad Request", validationError | |||
| Exit Sub | |||
| @@ -109,426 +109,11 @@ Class OrderApiController_Class | |||
| ' 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 | |||
| OrderMailer().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=""color:#ffffff; font-weight:700; font-size:18px;"">KCI Purple Envelope Order Confirmation</span>" & _ | |||
| "</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 | |||
| Dim purpleBlueProviderLine | |||
| purpleBlueProviderLine = LabelPurpleBlueProvider(model.PurpleBlueProvider) | |||
| If purpleBlueProviderLine = "Other" And Len(Trim(model.PurpleBlueProviderOther & "")) > 0 Then | |||
| purpleBlueProviderLine = model.PurpleBlueProviderOther | |||
| End If | |||
| html = html & EmailSection("Purple Ballot Envelopes", _ | |||
| EmailRow("Envelope stock", LabelEnvelopeStock(model.PurpleEnvelopeStock)) & _ | |||
| EmailRow("Print option", LabelPrintOption(model.PurplePrintOption)) & _ | |||
| EmailRow("Also wants KCI blue envelopes", EmailYesNo(model.PurpleWantsBlueEnvelopes)) & _ | |||
| EmailRow("Blue envelope provider", purpleBlueProviderLine) & _ | |||
| EmailRow("Print style", LabelPrintStyle(model.PurplePrintStyle)) & _ | |||
| EmailRow("Color coding by", LabelColorCodingBy(model.PurpleColorCodingBy)) & _ | |||
| EmailRow("Number of precincts", model.PurplePrecinctCount) & _ | |||
| EmailRow("Colors requested", model.PurpleColorNames) & _ | |||
| EmailRow("Track with TrackMI Ballot", EmailYesNo(model.PurpleTrackMIBallot)) & _ | |||
| EmailRow("Extra envelopes requested", model.PurpleExtraEnvelopeQty)) | |||
| End If | |||
| 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 | |||
| bluePermitLine = "" | |||
| 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 | |||
| bluePermitLine = bluePermitLine & EmailRow("Nonprofit authorization code", model.BlueNonprofitAuthCode) | |||
| End If | |||
| End If | |||
| html = html & EmailSection("Blue AV Envelopes", _ | |||
| EmailRow("Print permit", EmailYesNo(model.BluePrintPermit)) & _ | |||
| EmailRow("Permit ownership", LabelPermitOwnership(model.BluePermitOwnership)) & _ | |||
| EmailRow("Confirmed nonprofit status with USPS", EmailYesNo(model.BlueHasNonprofitStatus)) & _ | |||
| bluePermitLine) | |||
| 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("Estimated quantity", model.MailingEstimatedQuantity) & _ | |||
| EmailRow("Preferred pickup date", EmailDate(model.MailingPickupDate))) | |||
| End If | |||
| If NumOrZero(model.SecrecySleevesQty) > 0 Or NumOrZero(model.IVotedStickerRolls) > 0 Or NumOrZero(model.FutureVoterStickerRolls) > 0 Or Len(Trim(model.SpecialRequests & "")) > 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) & _ | |||
| EmailRow("Special requests", model.SpecialRequests)) | |||
| 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 "AddressesAndPermit" : LabelPrintOption = "Addresses and state of Michigan permit" | |||
| Case "PermitOnly" : LabelPrintOption = "State of Michigan permit only" | |||
| Case Else : LabelPrintOption = CStr(code & "") | |||
| End Select | |||
| End Function | |||
| Private Function LabelPurpleBlueProvider(code) | |||
| Select Case CStr(code & "") | |||
| Case "ElectionSource" : LabelPurpleBlueProvider = "ElectionSource" | |||
| Case "PSI" : LabelPurpleBlueProvider = "PSI" | |||
| Case "Spectrum" : LabelPurpleBlueProvider = "Spectrum" | |||
| Case "Other" : LabelPurpleBlueProvider = "Other" | |||
| Case Else : LabelPurpleBlueProvider = CStr(code & "") | |||
| End Select | |||
| End Function | |||
| Private Function LabelPermitOwnership(code) | |||
| Select Case CStr(code & "") | |||
| Case "KCIPermit" : LabelPermitOwnership = "KCI permit" | |||
| Case "OwnPermit" : LabelPermitOwnership = "My organization's permit" | |||
| Case Else : LabelPermitOwnership = 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 Ink 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 LabelPostageOption(code) | |||
| Select Case CStr(code & "") | |||
| Case "FirstClass" : LabelPostageOption = "Yes, at First Class Rate ($0.721/pc.)" | |||
| Case "NonprofitRate" : LabelPostageOption = "Yes, at Nonprofit Rate ($0.263/pc.)" | |||
| Case "PresortStandard" : LabelPostageOption = "Yes, at Presort Standard Rate ($0.473/pc.)" | |||
| 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, "PurpleEnvelopeStock", Array("KCIStock", "OwnStock") | |||
| RequireEnum errors, answers, "PurplePrintOption", Array("AddressesAndPermit", "PermitOnly") | |||
| RequireEnum errors, answers, "PurpleBlueProvider", Array("ElectionSource", "PSI", "Spectrum", "Other") | |||
| RequireEnum errors, answers, "PurplePrintStyle", Array("ColorCoding", "BlackOnly") | |||
| RequireEnum errors, answers, "PurpleColorCodingBy", Array("Precinct", "Election") | |||
| RequireEnum errors, answers, "BluePermitOwnership", Array("KCIPermit", "OwnPermit") | |||
| RequireEnum errors, answers, "MailingPostageOption", Array("FirstClass", "NonprofitRate", "PresortStandard", "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, "BlueHasNonprofitStatus" | |||
| RequireBoolean errors, answers, "MailingWantsService" | |||
| RequireNonNegativeInteger errors, answers, "PurplePrecinctCount" | |||
| RequireNonNegativeInteger errors, answers, "PurpleExtraEnvelopeQty" | |||
| RequireNonNegativeInteger errors, answers, "MailingEstimatedQuantity" | |||
| RequireNonNegativeInteger errors, answers, "SecrecySleevesQty" | |||
| RequireNonNegativeInteger errors, answers, "IVotedStickerRolls" | |||
| RequireNonNegativeInteger errors, answers, "FutureVoterStickerRolls" | |||
| 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.PurpleEnvelopeStock = AnswerValue(answers, "PurpleEnvelopeStock") | |||
| model.PurplePrintOption = AnswerValue(answers, "PurplePrintOption") | |||
| model.PurpleWantsBlueEnvelopes = AnswerValue(answers, "PurpleWantsBlueEnvelopes") | |||
| model.PurpleBlueProvider = AnswerValue(answers, "PurpleBlueProvider") | |||
| model.PurpleBlueProviderOther = AnswerValue(answers, "PurpleBlueProviderOther") | |||
| model.PurplePrintStyle = AnswerValue(answers, "PurplePrintStyle") | |||
| model.PurpleColorCodingBy = AnswerValue(answers, "PurpleColorCodingBy") | |||
| model.PurplePrecinctCount = AnswerValue(answers, "PurplePrecinctCount") | |||
| model.PurpleColorNames = AnswerValue(answers, "PurpleColorNames") | |||
| model.PurpleTrackMIBallot = AnswerValue(answers, "PurpleTrackMIBallot") | |||
| model.PurpleExtraEnvelopeQty = AnswerValue(answers, "PurpleExtraEnvelopeQty") | |||
| ' Blue envelope | |||
| model.BlueWantsQuote = AnswerValue(answers, "BlueWantsQuote") | |||
| model.BluePrintPermit = AnswerValue(answers, "BluePrintPermit") | |||
| model.BluePermitOwnership = AnswerValue(answers, "BluePermitOwnership") | |||
| model.BlueHasNonprofitStatus = AnswerValue(answers, "BlueHasNonprofitStatus") | |||
| model.BlueNonprofitAuthCode = AnswerValue(answers, "BlueNonprofitAuthCode") | |||
| model.BluePermitCity = AnswerValue(answers, "BluePermitCity") | |||
| model.BluePermitNumber = AnswerValue(answers, "BluePermitNumber") | |||
| ' Mailing service | |||
| model.MailingWantsService = AnswerValue(answers, "MailingWantsService") | |||
| model.MailingPostageOption = AnswerValue(answers, "MailingPostageOption") | |||
| model.MailingHasNonprofitStatus = AnswerValue(answers, "MailingHasNonprofitStatus") | |||
| model.MailingEstimatedQuantity = AnswerValue(answers, "MailingEstimatedQuantity") | |||
| model.MailingPickupDate = AnswerValue(answers, "MailingPickupDate") | |||
| ' Additional items | |||
| model.SecrecySleevesQty = AnswerValue(answers, "SecrecySleevesQty") | |||
| model.IVotedStickerRolls = AnswerValue(answers, "IVotedStickerRolls") | |||
| model.FutureVoterStickerRolls = AnswerValue(answers, "FutureVoterStickerRolls") | |||
| model.SpecialRequests = AnswerValue(answers, "SpecialRequests") | |||
| 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 | |||
| @@ -84,52 +84,12 @@ Class RequestOrderController_Class | |||
| End If | |||
| Dim token : token = OrdersRepository().CreateOrder(email, jurisdictionNumber) | |||
| SendOrderContinuationEmail email, token | |||
| OrderMailer().SendContinuationEmail email, token | |||
| Flash().Success = "Check your email for a link to continue your order." | |||
| Response.Redirect Routes().AppURL & "request-order" | |||
| End Sub | |||
| '------------------------------------------------------------------------------------------------------------------- | |||
| ' Private helpers | |||
| '------------------------------------------------------------------------------------------------------------------- | |||
| Private Function IsValidEmail(email) | |||
| Dim re | |||
| Set re = New RegExp | |||
| re.Pattern = "^[^\s@]+@[^\s@]+\.[^\s@]+$" | |||
| re.IgnoreCase = True | |||
| IsValidEmail = re.Test(email) | |||
| End Function | |||
| Private Sub AddValidationError(ByRef errorList, msg) | |||
| ReDim Preserve errorList(UBound(errorList) + 1) | |||
| errorList(UBound(errorList)) = msg | |||
| End Sub | |||
| Private Sub SendOrderContinuationEmail(email, token) | |||
| Dim continueUrl | |||
| continueUrl = Routes().AppURL & "order/continue?token=" & Server.URLEncode(token) | |||
| 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 = "Continue your Purple Envelope order" | |||
| mail.IsBodyHTML = True | |||
| mail.Body = "<p>Click the link below to continue your order:</p>" & _ | |||
| "<p><a href=""" & H(continueUrl) & """>" & H(continueUrl) & "</a></p>" & _ | |||
| "<p>This link expires in " & H(GetAppSetting("OrderTokenExpirationHours")) & _ | |||
| " hours and can only be used once.</p>" | |||
| mail.AddRecipient "To", email | |||
| mail.Send | |||
| End Sub | |||
| End Class | |||
| Dim RequestOrderController_Class__Singleton | |||
| @@ -2,6 +2,9 @@ | |||
| <!--#include file="../models/JurisdictionValidator.asp" --> | |||
| <!--#include file="../models/POBO_OrderDetails.asp" --> | |||
| <!--#include file="../models/OrderDetailsRepository.asp" --> | |||
| <!--#include file="../models/OrderDetailsLabels.asp" --> | |||
| <!--#include file="../models/OrderDetailsAnswersMapper.asp" --> | |||
| <!--#include file="../models/OrderMailer.asp" --> | |||
| <!--#include file="HomeController.asp" --> | |||
| <!--#include file="ErrorController.asp" --> | |||
| <!--#include file="RequestOrderController.asp" --> | |||
| @@ -0,0 +1,91 @@ | |||
| <% | |||
| '======================================================================================================================= | |||
| ' Maps a parsed-JSON answers Dictionary (from the SurveyJS order-details form) onto a | |||
| ' POBO_OrderDetails instance, and validates it first - the client-side SurveyJS | |||
| ' required/enum rules in continue.asp are a UX convenience, not something the server can trust. | |||
| '======================================================================================================================= | |||
| ' Returns "" if valid, or a "; "-joined list of validation error messages. | |||
| Function ValidateOrderDetailsAnswers(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, "PurpleEnvelopeStock", Array("KCIStock", "OwnStock") | |||
| RequireEnum errors, answers, "PurplePrintOption", Array("AddressesAndPermit", "PermitOnly") | |||
| RequireEnum errors, answers, "PurpleBlueProvider", Array("ElectionSource", "PSI", "Spectrum", "Other") | |||
| RequireEnum errors, answers, "PurplePrintStyle", Array("ColorCoding", "BlackOnly") | |||
| RequireEnum errors, answers, "PurpleColorCodingBy", Array("Precinct", "Election") | |||
| RequireEnum errors, answers, "BluePermitOwnership", Array("KCIPermit", "OwnPermit") | |||
| RequireEnum errors, answers, "MailingPostageOption", Array("FirstClass", "NonprofitRate", "PresortStandard", "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, "BlueHasNonprofitStatus" | |||
| RequireBoolean errors, answers, "MailingWantsService" | |||
| RequireNonNegativeInteger errors, answers, "PurplePrecinctCount" | |||
| RequireNonNegativeInteger errors, answers, "PurpleExtraEnvelopeQty" | |||
| RequireNonNegativeInteger errors, answers, "MailingEstimatedQuantity" | |||
| RequireNonNegativeInteger errors, answers, "SecrecySleevesQty" | |||
| RequireNonNegativeInteger errors, answers, "IVotedStickerRolls" | |||
| RequireNonNegativeInteger errors, answers, "FutureVoterStickerRolls" | |||
| RequireValidDate errors, answers, "MailingPickupDate" | |||
| If UBound(errors) >= 0 Then | |||
| ValidateOrderDetailsAnswers = Join(errors, "; ") | |||
| Else | |||
| ValidateOrderDetailsAnswers = "" | |||
| End If | |||
| End Function | |||
| Sub MapOrderDetailsAnswers(ByRef model, answers) | |||
| ' Contact | |||
| model.ContactName = DictValue(answers, "ContactName") | |||
| model.Municipality = DictValue(answers, "Municipality") | |||
| model.Phone = DictValue(answers, "Phone") | |||
| ' Purple envelope | |||
| model.PurpleWantsQuote = DictValue(answers, "PurpleWantsQuote") | |||
| model.PurpleEnvelopeStock = DictValue(answers, "PurpleEnvelopeStock") | |||
| model.PurplePrintOption = DictValue(answers, "PurplePrintOption") | |||
| model.PurpleWantsBlueEnvelopes = DictValue(answers, "PurpleWantsBlueEnvelopes") | |||
| model.PurpleBlueProvider = DictValue(answers, "PurpleBlueProvider") | |||
| model.PurpleBlueProviderOther = DictValue(answers, "PurpleBlueProviderOther") | |||
| model.PurplePrintStyle = DictValue(answers, "PurplePrintStyle") | |||
| model.PurpleColorCodingBy = DictValue(answers, "PurpleColorCodingBy") | |||
| model.PurplePrecinctCount = DictValue(answers, "PurplePrecinctCount") | |||
| model.PurpleColorNames = DictValue(answers, "PurpleColorNames") | |||
| model.PurpleTrackMIBallot = DictValue(answers, "PurpleTrackMIBallot") | |||
| model.PurpleExtraEnvelopeQty = DictValue(answers, "PurpleExtraEnvelopeQty") | |||
| ' Blue envelope | |||
| model.BlueWantsQuote = DictValue(answers, "BlueWantsQuote") | |||
| model.BluePrintPermit = DictValue(answers, "BluePrintPermit") | |||
| model.BluePermitOwnership = DictValue(answers, "BluePermitOwnership") | |||
| model.BlueHasNonprofitStatus = DictValue(answers, "BlueHasNonprofitStatus") | |||
| model.BlueNonprofitAuthCode = DictValue(answers, "BlueNonprofitAuthCode") | |||
| model.BluePermitCity = DictValue(answers, "BluePermitCity") | |||
| model.BluePermitNumber = DictValue(answers, "BluePermitNumber") | |||
| ' Mailing service | |||
| model.MailingWantsService = DictValue(answers, "MailingWantsService") | |||
| model.MailingPostageOption = DictValue(answers, "MailingPostageOption") | |||
| model.MailingHasNonprofitStatus = DictValue(answers, "MailingHasNonprofitStatus") | |||
| model.MailingEstimatedQuantity = DictValue(answers, "MailingEstimatedQuantity") | |||
| model.MailingPickupDate = DictValue(answers, "MailingPickupDate") | |||
| ' Additional items | |||
| model.SecrecySleevesQty = DictValue(answers, "SecrecySleevesQty") | |||
| model.IVotedStickerRolls = DictValue(answers, "IVotedStickerRolls") | |||
| model.FutureVoterStickerRolls = DictValue(answers, "FutureVoterStickerRolls") | |||
| model.SpecialRequests = DictValue(answers, "SpecialRequests") | |||
| End Sub | |||
| %> | |||
| @@ -0,0 +1,75 @@ | |||
| <% | |||
| '======================================================================================================================= | |||
| ' 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. | |||
| '======================================================================================================================= | |||
| Function LabelPrintOption(code) | |||
| Select Case CStr(code & "") | |||
| Case "AddressesAndPermit" : LabelPrintOption = "Addresses and state of Michigan permit" | |||
| Case "PermitOnly" : LabelPrintOption = "State of Michigan permit only" | |||
| Case Else : LabelPrintOption = CStr(code & "") | |||
| End Select | |||
| End Function | |||
| Function LabelPurpleBlueProvider(code) | |||
| Select Case CStr(code & "") | |||
| Case "ElectionSource" : LabelPurpleBlueProvider = "ElectionSource" | |||
| Case "PSI" : LabelPurpleBlueProvider = "PSI" | |||
| Case "Spectrum" : LabelPurpleBlueProvider = "Spectrum" | |||
| Case "Other" : LabelPurpleBlueProvider = "Other" | |||
| Case Else : LabelPurpleBlueProvider = CStr(code & "") | |||
| End Select | |||
| End Function | |||
| Function LabelPermitOwnership(code) | |||
| Select Case CStr(code & "") | |||
| Case "KCIPermit" : LabelPermitOwnership = "KCI permit" | |||
| Case "OwnPermit" : LabelPermitOwnership = "My organization's permit" | |||
| Case Else : LabelPermitOwnership = CStr(code & "") | |||
| End Select | |||
| End Function | |||
| 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 | |||
| Function LabelPrintStyle(code) | |||
| Select Case CStr(code & "") | |||
| Case "ColorCoding" : LabelPrintStyle = "Color Coding" | |||
| Case "BlackOnly" : LabelPrintStyle = "Black Ink Only" | |||
| Case Else : LabelPrintStyle = CStr(code & "") | |||
| End Select | |||
| End Function | |||
| Function LabelColorCodingBy(code) | |||
| Select Case CStr(code & "") | |||
| Case "Precinct" : LabelColorCodingBy = "Precinct" | |||
| Case "Election" : LabelColorCodingBy = "Election" | |||
| Case Else : LabelColorCodingBy = CStr(code & "") | |||
| End Select | |||
| End Function | |||
| Function LabelPostageOption(code) | |||
| Select Case CStr(code & "") | |||
| Case "FirstClass" : LabelPostageOption = "Yes, at First Class Rate ($0.721/pc.)" | |||
| Case "NonprofitRate" : LabelPostageOption = "Yes, at Nonprofit Rate ($0.263/pc.)" | |||
| Case "PresortStandard" : LabelPostageOption = "Yes, at Presort Standard Rate ($0.473/pc.)" | |||
| Case "No" : LabelPostageOption = "No" | |||
| Case Else : LabelPostageOption = CStr(code & "") | |||
| End Select | |||
| End Function | |||
| 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 | |||
| %> | |||
| @@ -0,0 +1,214 @@ | |||
| <% | |||
| '======================================================================================================================= | |||
| ' Order-related outbound email: the "continue your order" link and the order-details | |||
| ' confirmation sent after SubmitOrderDetails. | |||
| '======================================================================================================================= | |||
| Class OrderMailer_Class | |||
| Public Sub SendContinuationEmail(email, token) | |||
| Dim continueUrl | |||
| continueUrl = Routes().AppURL & "order/continue?token=" & Server.URLEncode(token) | |||
| Dim mail : Set mail = NewConfiguredMail() | |||
| mail.Subject = "Continue your Purple Envelope order" | |||
| mail.IsBodyHTML = True | |||
| mail.Body = "<p>Click the link below to continue your order:</p>" & _ | |||
| "<p><a href=""" & H(continueUrl) & """>" & H(continueUrl) & "</a></p>" & _ | |||
| "<p>This link expires in " & H(GetAppSetting("OrderTokenExpirationHours")) & _ | |||
| " hours and can only be used once.</p>" | |||
| mail.AddRecipient "To", email | |||
| mail.Send | |||
| End Sub | |||
| ' Best-effort: the order is already safely persisted by the time a caller reaches this - | |||
| ' an SMTP hiccup here should not turn a successful submission into a failed one. | |||
| Public Sub SendOrderDetailsEmail(order, model) | |||
| On Error Resume Next | |||
| Dim mail : Set mail = NewConfiguredMail() | |||
| 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 helpers | |||
| '------------------------------------------------------------------------------------------------------------------- | |||
| Private Function NewConfiguredMail() | |||
| 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") | |||
| Set NewConfiguredMail = mail | |||
| End Function | |||
| 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=""color:#ffffff; font-weight:700; font-size:18px;"">KCI Purple Envelope Order Confirmation</span>" & _ | |||
| "</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 | |||
| Dim purpleBlueProviderLine | |||
| purpleBlueProviderLine = LabelPurpleBlueProvider(model.PurpleBlueProvider) | |||
| If purpleBlueProviderLine = "Other" And Len(Trim(model.PurpleBlueProviderOther & "")) > 0 Then | |||
| purpleBlueProviderLine = model.PurpleBlueProviderOther | |||
| End If | |||
| html = html & EmailSection("Purple Ballot Envelopes", _ | |||
| EmailRow("Envelope stock", LabelEnvelopeStock(model.PurpleEnvelopeStock)) & _ | |||
| EmailRow("Print option", LabelPrintOption(model.PurplePrintOption)) & _ | |||
| EmailRow("Also wants KCI blue envelopes", EmailYesNo(model.PurpleWantsBlueEnvelopes)) & _ | |||
| EmailRow("Blue envelope provider", purpleBlueProviderLine) & _ | |||
| EmailRow("Print style", LabelPrintStyle(model.PurplePrintStyle)) & _ | |||
| EmailRow("Color coding by", LabelColorCodingBy(model.PurpleColorCodingBy)) & _ | |||
| EmailRow("Number of precincts", model.PurplePrecinctCount) & _ | |||
| EmailRow("Colors requested", model.PurpleColorNames) & _ | |||
| EmailRow("Track with TrackMI Ballot", EmailYesNo(model.PurpleTrackMIBallot)) & _ | |||
| EmailRow("Extra envelopes requested", model.PurpleExtraEnvelopeQty)) | |||
| End If | |||
| 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 | |||
| bluePermitLine = "" | |||
| 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 | |||
| bluePermitLine = bluePermitLine & EmailRow("Nonprofit authorization code", model.BlueNonprofitAuthCode) | |||
| End If | |||
| End If | |||
| html = html & EmailSection("Blue AV Envelopes", _ | |||
| EmailRow("Print permit", EmailYesNo(model.BluePrintPermit)) & _ | |||
| EmailRow("Permit ownership", LabelPermitOwnership(model.BluePermitOwnership)) & _ | |||
| EmailRow("Confirmed nonprofit status with USPS", EmailYesNo(model.BlueHasNonprofitStatus)) & _ | |||
| bluePermitLine) | |||
| 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("Estimated quantity", model.MailingEstimatedQuantity) & _ | |||
| EmailRow("Preferred pickup date", EmailDate(model.MailingPickupDate))) | |||
| End If | |||
| If NumOrZero(model.SecrecySleevesQty) > 0 Or NumOrZero(model.IVotedStickerRolls) > 0 Or NumOrZero(model.FutureVoterStickerRolls) > 0 Or Len(Trim(model.SpecialRequests & "")) > 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) & _ | |||
| EmailRow("Special requests", model.SpecialRequests)) | |||
| 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 | |||
| End Class | |||
| Dim OrderMailer_Class__Singleton | |||
| Function OrderMailer() | |||
| If IsEmpty(OrderMailer_Class__Singleton) Then | |||
| Set OrderMailer_Class__Singleton = New OrderMailer_Class | |||
| End If | |||
| Set OrderMailer = OrderMailer_Class__Singleton | |||
| End Function | |||
| %> | |||
| @@ -16,6 +16,8 @@ | |||
| <!--#include file="../Core/lib.CDOEmail.asp"--> | |||
| <!--#include file="../Core/lib.Upload.asp"--> | |||
| <!--#include file="../Core/lib.json.asp"--> | |||
| <!--#include file="../Core/lib.JsonResponse.asp"--> | |||
| <!--#include file="../Core/lib.Validations.asp"--> | |||
| <!--#include file="../Core/lib.helpers.asp"--> | |||
| <!--#include file="../Core/lib.crypto.helper.asp"--> | |||
| <!--#include file="../Core/lib.Enumerable.asp"--> | |||
| @@ -0,0 +1,20 @@ | |||
| <% | |||
| '======================================================================================================================= | |||
| ' Small helpers for controllers that only ever return JSON (useLayout = False), so each new | |||
| ' JSON API controller doesn't reimplement error-response formatting and string escaping. | |||
| '======================================================================================================================= | |||
| Sub WriteJsonError(statusLine, message) | |||
| Response.Status = statusLine | |||
| Response.Write "{""success"":false,""error"":" & JsonEncodeString(message) & "}" | |||
| End Sub | |||
| 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 | |||
| %> | |||
| @@ -247,4 +247,85 @@ Class Validator_Class | |||
| m_errors(ubound(m_errors)) = msg | |||
| End Sub | |||
| End Class | |||
| '----------------------------------------------------------------------------------------------------------------------- | |||
| ' Dictionary-based answer validation | |||
| ' For validating a Scripting.Dictionary of loosely-typed values (e.g. a parsed JSON POST body) | |||
| ' against field rules, accumulating messages into a ByRef errors() array - a different shape | |||
| ' of validation than the instance/property Validator_Class above, which needs a live object | |||
| ' instance and an Eval'able property name. | |||
| '----------------------------------------------------------------------------------------------------------------------- | |||
| Function IsValidEmail(email) | |||
| Dim re | |||
| Set re = New RegExp | |||
| re.Pattern = "^[^\s@]+@[^\s@]+\.[^\s@]+$" | |||
| re.IgnoreCase = True | |||
| IsValidEmail = re.Test(email) | |||
| End Function | |||
| Sub AddValidationError(ByRef errors, msg) | |||
| ReDim Preserve errors(UBound(errors) + 1) | |||
| errors(UBound(errors)) = msg | |||
| End Sub | |||
| ' Returns the value for key in a Scripting.Dictionary, or Null if not present (e.g. a | |||
| ' SurveyJS question skipped via visibleIf logic and so never included in the result data). | |||
| Function DictValue(dict, key) | |||
| If dict.Exists(key) Then | |||
| DictValue = dict.Item(key) | |||
| Else | |||
| DictValue = Null | |||
| End If | |||
| End Function | |||
| ' Required - always checked. | |||
| Sub RequireNonEmptyString(ByRef errors, dict, key, msg) | |||
| Dim v : v = DictValue(dict, key) | |||
| If IsNull(v) Or Len(Trim(v & "")) = 0 Then AddValidationError errors, msg | |||
| End Sub | |||
| ' Only checked when present - callers may have fields that are conditionally hidden | |||
| ' client-side, so a missing value just means the field wasn't shown, not an invalid submission. | |||
| Sub RequireEnum(ByRef errors, dict, key, allowedValues) | |||
| Dim v : v = DictValue(dict, 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 | |||
| Sub RequireBoolean(ByRef errors, dict, key) | |||
| Dim v : v = DictValue(dict, key) | |||
| If Not IsNull(v) Then | |||
| If TypeName(v) <> "Boolean" Then AddValidationError errors, key & " must be true or false." | |||
| End If | |||
| End Sub | |||
| Sub RequireNonNegativeInteger(ByRef errors, dict, key) | |||
| Dim v : v = DictValue(dict, 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 | |||
| Sub RequireValidDate(ByRef errors, dict, key) | |||
| Dim v : v = DictValue(dict, 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 | |||
| %> | |||
Powered by TurnKey Linux.