Skip to content

Commit

Permalink
Merge pull request #164 from guardian/email_changes
Browse files Browse the repository at this point in the history
Email changes
  • Loading branch information
pvighi committed Aug 1, 2018
2 parents db47d3c + 6417309 commit f3adbea
Show file tree
Hide file tree
Showing 32 changed files with 924 additions and 83 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ lazy val `identity-retention` = all(project in file("handlers/identity-retention

lazy val `new-product-api` = all(project in file("handlers/new-product-api"))
.enablePlugins(RiffRaffArtifact)
.dependsOn(zuora, handler, effectsDepIncludingTestFolder, testDep)
.dependsOn(zuora, handler, `effects-sqs`, effectsDepIncludingTestFolder, testDep)

lazy val `zuora-retention` = all(project in file("handlers/zuora-retention"))
.enablePlugins(RiffRaffArtifact)
Expand Down
8 changes: 8 additions & 0 deletions handlers/new-product-api/cfn.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ Resources:
Resource:
- !Sub arn:aws:s3:::gu-reader-revenue-private/membership/support-service-lambdas/${Stage}/zuoraRest-${Stage}.*.json
- !Sub arn:aws:s3:::gu-reader-revenue-private/membership/support-service-lambdas/${Stage}/trustedApi-${Stage}.*.json
- PolicyName: SQSAddEmailRequest
PolicyDocument:
Statement:
- Effect: Allow
Action:
- sqs:GetQueueUrl
- sqs:SendMessage
Resource: !Sub "arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:contributions-thanks"

AddSubscriptionLambda:
Type: AWS::Lambda::Function
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ case class AddSubscriptionRequest(
startDate: LocalDate,
acquisitionSource: AcquisitionSource,
createdByCSR: CreatedByCSR,
amountMinorUnits: Int,
amountMinorUnits: AmountMinorUnits,
acquisitionCase: CaseId
)

case class AmountMinorUnits(value: Int) extends AnyVal
case class CaseId(value: String) extends AnyVal
case class AcquisitionSource(value: String) extends AnyVal
case class CreatedByCSR(value: String) extends AnyVal
Expand All @@ -38,7 +38,7 @@ object AddSubscriptionRequest {
startDate = parsedStartDate,
acquisitionSource = AcquisitionSource(this.acquisitionSource),
createdByCSR = CreatedByCSR(this.createdByCSR),
amountMinorUnits = this.amountMinorUnits,
amountMinorUnits = AmountMinorUnits(amountMinorUnits),
CaseId(acquisitionCase)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,76 @@ package com.gu.newproduct.api.addsubscription
import java.io.{InputStream, OutputStream}

import com.amazonaws.services.lambda.runtime.Context
import com.gu.effects.sqs.AwsSQSSend
import com.gu.effects.sqs.AwsSQSSend.QueueName
import com.gu.effects.{GetFromS3, RawEffects}
import com.gu.newproduct.api.addsubscription.TypeConvert._
import com.gu.newproduct.api.addsubscription.email.{ContributionFields, EtSqsSend, SendConfirmationEmail}
import com.gu.newproduct.api.addsubscription.email.SendConfirmationEmail.ContributionsEmailData
import com.gu.newproduct.api.addsubscription.validation._
import com.gu.newproduct.api.addsubscription.zuora.{CreateSubscription, GetAccount}
import com.gu.newproduct.api.addsubscription.zuora.CreateSubscription.WireModel.{WireCreateRequest, WireSubscription}
import com.gu.newproduct.api.addsubscription.zuora.CreateSubscription.{CreateReq, SubscriptionName}
import com.gu.newproduct.api.addsubscription.zuora.GetAccount.WireModel.ZuoraAccount
import com.gu.newproduct.api.addsubscription.zuora.GetBillToContact.WireModel.GetBillToResponse
import com.gu.newproduct.api.addsubscription.zuora.GetPaymentMethod.DirectDebit
import com.gu.newproduct.api.addsubscription.zuora.{CreateSubscription, GetAccount, GetBillToContact}
import com.gu.util.Logging
import com.gu.util.apigateway.ApiGatewayHandler.{LambdaIO, Operation}
import com.gu.util.apigateway.ResponseModels.ApiResponse
import com.gu.util.apigateway.{ApiGatewayHandler, ApiGatewayRequest, ApiGatewayResponse}
import com.gu.util.config.LoadConfigModule.StringFromS3
import com.gu.util.config.{LoadConfigModule, Stage}
import com.gu.util.reader.AsyncTypes._
import com.gu.util.reader.Types._
import com.gu.util.resthttp.Types.ClientFailableOp
import com.gu.util.zuora.{ZuoraRestConfig, ZuoraRestRequestMaker}
import okhttp3.{Request, Response}

import scala.concurrent.Future
object Handler extends Logging {

// Referenced in Cloudformation
def apply(inputStream: InputStream, outputStream: OutputStream, context: Context): Unit =
ApiGatewayHandler(LambdaIO(inputStream, outputStream, context)) {
Steps.operationForEffects(RawEffects.response, RawEffects.stage, GetFromS3.fetchString)
}

}

object Steps {

def addSubscriptionSteps(
prerequesiteCheck: AddSubscriptionRequest => ApiGatewayOp[Unit],
createMonthlyContribution: CreateReq => ClientFailableOp[SubscriptionName]
)(apiGatewayRequest: ApiGatewayRequest): ApiResponse = {
prerequisiteCheck: AddSubscriptionRequest => AsyncApiGatewayOp[ValidatedFields],
createMonthlyContribution: CreateReq => ClientFailableOp[SubscriptionName],
sendConfirmationEmail: ContributionsEmailData => AsyncApiGatewayOp[Unit],
)(apiGatewayRequest: ApiGatewayRequest): Future[ApiResponse] = {
(for {
request <- apiGatewayRequest.bodyAsCaseClass[AddSubscriptionRequest]().withLogging("parsed request")
_ <- prerequesiteCheck(request)
request <- apiGatewayRequest.bodyAsCaseClass[AddSubscriptionRequest]().withLogging("parsed request").toAsync
validatedFields <- prerequisiteCheck(request)

acceptanceDate = validatedFields.paymentMethod match {
case d: DirectDebit => request.startDate.plusDays(10)
case _ => request.startDate
}

req = CreateReq(
request.zuoraAccountId,
request.amountMinorUnits,
request.startDate,
acceptanceDate,
request.acquisitionCase,
request.acquisitionSource,
request.createdByCSR
)
subscriptionName <- createMonthlyContribution(req).toApiGatewayOp("create monthly contribution")
subscriptionName <- createMonthlyContribution(req).toAsyncApiGatewayOp("create monthly contribution")

contributionEmailData = ContributionsEmailData(
accountId = request.zuoraAccountId,
currency = validatedFields.currency,
paymentMethod = validatedFields.paymentMethod,
amountMinorUnits = req.amountMinorUnits,
firstPaymentDate = acceptanceDate
)
_ <- sendConfirmationEmail(contributionEmailData)
} yield ApiGatewayResponse(body = AddedSubscription(subscriptionName.value), statusCode = "200")).apiResponse
}

Expand All @@ -58,14 +82,28 @@ object Steps {
loadConfig = LoadConfigModule(stage, fetchString)
zuoraConfig <- loadConfig[ZuoraRestConfig].toApiGatewayOp("load zuora config")
zuoraClient = ZuoraRestRequestMaker(response, zuoraConfig)
sqsSend = AwsSQSSend(emailQueueFor(stage)) _
contributionsSqsSend = EtSqsSend[ContributionFields](sqsSend) _

getBillTo = GetBillToContact(zuoraClient.get[GetBillToResponse]) _
createMonthlyContribution = CreateSubscription(zuoraIds.monthly, zuoraClient.post[WireCreateRequest, WireSubscription]) _
contributionIds = List(zuoraIds.monthly.productRatePlanId, zuoraIds.annual.productRatePlanId)
prerequesiteCheck = PrerequisiteCheck(zuoraClient, contributionIds, RawEffects.now) _
configuredOp = Operation(
steps = addSubscriptionSteps(prerequesiteCheck, createMonthlyContribution),
getCurrentDate = () => RawEffects.now().toLocalDate
prerequisiteCheck = PrerequisiteCheck(zuoraClient, contributionIds, getCurrentDate) _
asyncPrerequisiteCheck = prerequisiteCheck.andThenConvertToAsync
sendConfirmationEmail = SendConfirmationEmail(contributionsSqsSend, getCurrentDate, getBillTo) _
configuredOp = Operation.async(
steps = addSubscriptionSteps(asyncPrerequisiteCheck, createMonthlyContribution, sendConfirmationEmail),
healthcheck = () =>
HealthCheck(GetAccount(zuoraClient.get[ZuoraAccount]), AccountIdentitys.accountIdentitys(stage))
)
} yield configuredOp

def emailQueueFor(stage: Stage) = stage match {
case Stage("PROD") => QueueName("contributions-thanks")
case Stage("CODE") => QueueName("contributions-thanks")
case _ => QueueName("contributions-thanks-dev")
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ import com.gu.util.apigateway.ApiGatewayResponse
import com.gu.util.apigateway.ResponseModels.ApiResponse
import com.gu.util.reader.Types.ApiGatewayOp.{ContinueProcessing, ReturnWithResponse}
import com.gu.util.reader.Types._
import com.gu.util.reader.AsyncTypes._
import com.gu.util.resthttp.Types.{ClientFailableOp, NotFound}

import scala.concurrent.Future

object TypeConvert {

implicit class TypeConvertClientOp[A](theEither: ClientFailableOp[A]) {
def toApiGatewayOp = theEither.toDisjunction.toApiGatewayOp(_)
implicit class TypeConvertClientOp[A](clientOp: ClientFailableOp[A]) {
def toApiGatewayOp = clientOp.toDisjunction.toApiGatewayOp(_)
}

implicit class TypeConvertClientOpAsync[A](clientOp: ClientFailableOp[A]) {
def toAsyncApiGatewayOp(action: String) = {
val apiGatewayOp = Future.successful(clientOp.toApiGatewayOp(action))
AsyncApiGatewayOp(apiGatewayOp)
}
}

implicit class ValidationToApiGatewayOp[A](validationResult: ValidationResult[A]) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.gu.newproduct.api.addsubscription.email

import java.time.LocalDate
import java.time.format.DateTimeFormatter

import com.gu.i18n.Currency
import com.gu.newproduct.api.addsubscription.AmountMinorUnits
import com.gu.newproduct.api.addsubscription.zuora.GetBillToContact.Contact
import com.gu.newproduct.api.addsubscription.zuora.GetPaymentMethod.DirectDebit
import play.api.libs.json.Json

case class ContributionFields(
EmailAddress: String,
created: String,
amount: String,
currency: String,
edition: String,
name: String,
product: String,
`account name`: Option[String] = None,
`account number`: Option[String] = None,
`sort code`: Option[String] = None,
`Mandate ID`: Option[String] = None,
`first payment date`: Option[String] = None,
`payment method`: Option[String] = None
)

object ContributionFields {
implicit val writes = Json.writes[ContributionFields]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.gu.newproduct.api.addsubscription.email

import play.api.libs.json.{Json, Writes}

case class CContactAttributes[A](SubscriberAttributes: A)

case class CTo[A](Address: String, SubscriberKey: String, ContactAttributes: CContactAttributes[A])

case class ETPayload[A](To: CTo[A], DataExtensionName: String)

object CContactAttributes {
implicit def writes[A: Writes] = Json.writes[CContactAttributes[A]]
}

object CTo {
implicit def writes[A: Writes] = Json.writes[CTo[A]]
}

object ETPayload {

implicit def writes[A: Writes] = Json.writes[ETPayload[A]]

def apply[A](email: String, fields: A): ETPayload[A] =
ETPayload(
To = CTo(email, email, CContactAttributes(fields)),
DataExtensionName = "regular-contribution-thank-you"
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.gu.newproduct.api.addsubscription.email

import com.gu.effects.sqs.AwsSQSSend.Payload
import com.gu.util.Logging
import play.api.libs.json.{Json, Writes}

import scala.concurrent.Future

object EtSqsSend extends Logging {

def apply[FIELDS: Writes](sqsSend: Payload => Future[Unit]) = { etPayload: ETPayload[FIELDS] =>
val payloadString = Json.prettyPrint(Json.toJson(etPayload))
sqsSend(Payload(payloadString))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.gu.newproduct.api.addsubscription.email

import java.time.LocalDate
import java.time.format.DateTimeFormatter

import com.gu.i18n.Currency
import com.gu.newproduct.api.addsubscription.zuora.GetBillToContact.Contact
import com.gu.newproduct.api.addsubscription.zuora.GetPaymentMethod.{DirectDebit, PaymentMethod}
import com.gu.newproduct.api.addsubscription.{AmountMinorUnits, ZuoraAccountId}
import com.gu.util.Logging
import com.gu.util.apigateway.ApiGatewayResponse
import com.gu.util.reader.Types.ApiGatewayOp.{ContinueProcessing, ReturnWithResponse}
import com.gu.util.resthttp.Types.ClientFailableOp
import com.gu.util.reader.AsyncTypes._
import com.gu.newproduct.api.addsubscription.TypeConvert._

import scala.concurrent.Future

object SendConfirmationEmail extends Logging {

case class ContributionsEmailData(
accountId: ZuoraAccountId,
currency: Currency,
paymentMethod: PaymentMethod,
amountMinorUnits: AmountMinorUnits,
firstPaymentDate: LocalDate
)

def apply(
etSqsSend: ETPayload[ContributionFields] => Future[Unit],
getCurrentDate: () => LocalDate,
getBillTo: ZuoraAccountId => ClientFailableOp[Contact]
)(data: ContributionsEmailData) = {

val response = for {
billTo <- getBillTo(data.accountId).toAsyncApiGatewayOp("getting billTo contact from Zuora")
maybeContributionFields = toContributionFields(getCurrentDate(), billTo, data)
etPayload <- toPayload(maybeContributionFields)
sendMessageResult <- etSqsSend(etPayload).toAsyncApiGatewayOp("sending sqs message")
} yield sendMessageResult

response.replace(ContinueProcessing(()))

}

def toPayload(maybeContributionFields: Option[ContributionFields]): AsyncApiGatewayOp[ETPayload[ContributionFields]] =
maybeContributionFields.map { fields =>
val payload = ETPayload(fields.EmailAddress, fields)
ContinueProcessing(payload).toAsync
}.getOrElse {
logger.info("Not enough data in zuora account to send contribution thank you email, skipping")
ReturnWithResponse(ApiGatewayResponse.successfulExecution).toAsync
}

def hyphenate(s: String) = s"${s.substring(0, 2)}-${s.substring(2, 4)}-${s.substring(4, 6)}"
def formatAmount(amount: AmountMinorUnits) = (amount.value / BigDecimal(100)).bigDecimal.stripTrailingZeros.toPlainString
val firstPaymentDateFormat = DateTimeFormatter.ofPattern("EEEE, d MMMM yyyy")

def toContributionFields(currentDate: LocalDate, billTo: Contact, data: ContributionsEmailData): Option[ContributionFields] = {

val maybeDirectDebit = data.paymentMethod match {
case d: DirectDebit => Some(d)
case _ => None
}
billTo.email.map { email =>
ContributionFields(
EmailAddress = email.value,
created = currentDate.toString,
amount = formatAmount(data.amountMinorUnits),
currency = data.currency.glyph,
edition = billTo.country.map(_.alpha2).getOrElse(""),
name = s"${billTo.firstName.value} ${billTo.lastName.value}",
product = "monthly-contribution",
`account name` = maybeDirectDebit.map(_.accountName.value),
`account number` = maybeDirectDebit.map(_.accountNumberMask.value),
`sort code` = maybeDirectDebit.map(x => hyphenate(x.sortCode.value)),
`Mandate ID` = maybeDirectDebit.map(_.mandateId.value),
`first payment date` = maybeDirectDebit.map { _ =>
data.firstPaymentDate.format(firstPaymentDateFormat)
},
`payment method` = maybeDirectDebit.map(_ => "Direct Debit")
)
}
}
}

Loading

0 comments on commit f3adbea

Please sign in to comment.