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 | On Error GoTo 0 | ||||
| Set answers = json().data | Set answers = json().data | ||||
| Dim validationError : validationError = ValidateAnswers(answers) | |||||
| Dim validationError : validationError = ValidateOrderDetailsAnswers(answers) | |||||
| If Len(validationError) > 0 Then | If Len(validationError) > 0 Then | ||||
| WriteJsonError "400 Bad Request", validationError | WriteJsonError "400 Bad Request", validationError | ||||
| Exit Sub | Exit Sub | ||||
| @@ -109,426 +109,11 @@ Class OrderApiController_Class | |||||
| ' Best-effort: the order is already safely persisted at this point, so an SMTP hiccup | ' 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. | ' should not turn a successful submission into a failed one. | ||||
| SendOrderDetailsEmail order, model | |||||
| OrderMailer().SendOrderDetailsEmail order, model | |||||
| Response.Write "{""success"":true}" | Response.Write "{""success"":true}" | ||||
| End Sub | 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 | End Class | ||||
| ' Singleton instance | ' Singleton instance | ||||
| @@ -84,52 +84,12 @@ Class RequestOrderController_Class | |||||
| End If | End If | ||||
| Dim token : token = OrdersRepository().CreateOrder(email, jurisdictionNumber) | 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." | Flash().Success = "Check your email for a link to continue your order." | ||||
| Response.Redirect Routes().AppURL & "request-order" | Response.Redirect Routes().AppURL & "request-order" | ||||
| End Sub | 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 | End Class | ||||
| Dim RequestOrderController_Class__Singleton | Dim RequestOrderController_Class__Singleton | ||||
| @@ -2,6 +2,9 @@ | |||||
| <!--#include file="../models/JurisdictionValidator.asp" --> | <!--#include file="../models/JurisdictionValidator.asp" --> | ||||
| <!--#include file="../models/POBO_OrderDetails.asp" --> | <!--#include file="../models/POBO_OrderDetails.asp" --> | ||||
| <!--#include file="../models/OrderDetailsRepository.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="HomeController.asp" --> | ||||
| <!--#include file="ErrorController.asp" --> | <!--#include file="ErrorController.asp" --> | ||||
| <!--#include file="RequestOrderController.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.CDOEmail.asp"--> | ||||
| <!--#include file="../Core/lib.Upload.asp"--> | <!--#include file="../Core/lib.Upload.asp"--> | ||||
| <!--#include file="../Core/lib.json.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.helpers.asp"--> | ||||
| <!--#include file="../Core/lib.crypto.helper.asp"--> | <!--#include file="../Core/lib.crypto.helper.asp"--> | ||||
| <!--#include file="../Core/lib.Enumerable.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 | m_errors(ubound(m_errors)) = msg | ||||
| End Sub | End Sub | ||||
| End Class | 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.