Sfoglia il codice sorgente

Split OrderApiController into focused model classes

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
Daniel Covington 2 settimane fa
parent
commit
58c6eb22da
9 ha cambiato i file con 489 aggiunte e 458 eliminazioni
  1. +2
    -417
      app/controllers/OrderApiController.asp
  2. +1
    -41
      app/controllers/RequestOrderController.asp
  3. +3
    -0
      app/controllers/autoload_controllers.asp
  4. +91
    -0
      app/models/OrderDetailsAnswersMapper.asp
  5. +75
    -0
      app/models/OrderDetailsLabels.asp
  6. +214
    -0
      app/models/OrderMailer.asp
  7. +2
    -0
      core/autoload_core.asp
  8. +20
    -0
      core/lib.JsonResponse.asp
  9. +81
    -0
      core/lib.Validations.asp

+ 2
- 417
app/controllers/OrderApiController.asp Vedi File

@@ -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>" & _
" &middot; 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 &middot; Addressing &amp; 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


+ 1
- 41
app/controllers/RequestOrderController.asp Vedi File

@@ -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


+ 3
- 0
app/controllers/autoload_controllers.asp Vedi File

@@ -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" -->


+ 91
- 0
app/models/OrderDetailsAnswersMapper.asp Vedi File

@@ -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
%>

+ 75
- 0
app/models/OrderDetailsLabels.asp Vedi File

@@ -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
%>

+ 214
- 0
app/models/OrderMailer.asp Vedi File

@@ -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>" & _
" &middot; 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 &middot; Addressing &amp; 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
%>

+ 2
- 0
core/autoload_core.asp Vedi File

@@ -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"-->


+ 20
- 0
core/lib.JsonResponse.asp Vedi File

@@ -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
%>

+ 81
- 0
core/lib.Validations.asp Vedi File

@@ -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
%>

Loading…
Annulla
Salva

Powered by TurnKey Linux.