Skip to content

Commit

Permalink
Merge d3c5432 into 980273d
Browse files Browse the repository at this point in the history
  • Loading branch information
pvighi committed Aug 16, 2018
2 parents 980273d + d3c5432 commit 40567c2
Show file tree
Hide file tree
Showing 37 changed files with 1,002 additions and 303 deletions.
Expand Up @@ -6,7 +6,7 @@ import com.gu.newproduct.api.productcatalog.PlanId
import play.api.libs.json.{JsError, JsSuccess, Json, Reads}
import scalaz._
import Scalaz._
import scala.util.{Failure, Success, Try}
import scala.util.{Try}

case class ZuoraAccountId(value: String) extends AnyVal

Expand All @@ -15,7 +15,7 @@ case class AddSubscriptionRequest(
startDate: LocalDate,
acquisitionSource: AcquisitionSource,
createdByCSR: CreatedByCSR,
amountMinorUnits: AmountMinorUnits,
amountMinorUnits: Option[AmountMinorUnits],
acquisitionCase: CaseId,
planId: PlanId
)
Expand All @@ -31,7 +31,7 @@ object AddSubscriptionRequest {
startDate: String,
acquisitionSource: String,
createdByCSR: String,
amountMinorUnits: Int,
amountMinorUnits: Option[Int],
acquisitionCase: String,
planId: String
) {
Expand All @@ -44,7 +44,7 @@ object AddSubscriptionRequest {
startDate = parsedDate,
acquisitionSource = AcquisitionSource(this.acquisitionSource),
createdByCSR = CreatedByCSR(this.createdByCSR),
amountMinorUnits = AmountMinorUnits(amountMinorUnits),
amountMinorUnits = amountMinorUnits.map(AmountMinorUnits),
CaseId(acquisitionCase),
parsedPlanId
)
Expand Down
Expand Up @@ -12,18 +12,21 @@ import com.gu.newproduct.api.addsubscription.TypeConvert._
import com.gu.newproduct.api.addsubscription.ZuoraIds.ProductRatePlanId
import com.gu.newproduct.api.addsubscription.email.SendConfirmationEmail.ContributionsEmailData
import com.gu.newproduct.api.addsubscription.email.{ContributionFields, EtSqsSend, SendConfirmationEmail}
import com.gu.newproduct.api.addsubscription.validation.ValidateRequest.ValidatableFields
import com.gu.newproduct.api.addsubscription.validation.Validation._
import com.gu.newproduct.api.addsubscription.validation._
import com.gu.newproduct.api.addsubscription.validation.contribution.ContributionValidations.ValidatableFields
import com.gu.newproduct.api.addsubscription.validation.contribution.{ContributionAccountValidation, ContributionCustomerData, ContributionValidations, GetContributionCustomerData}
import com.gu.newproduct.api.addsubscription.validation.voucher.{GetVoucherCustomerData, ValidateContactsForVoucher, VoucherAccountValidation, VoucherCustomerData}
import com.gu.newproduct.api.addsubscription.validation.{ValidationResult, _}
import com.gu.newproduct.api.addsubscription.zuora.CreateSubscription.WireModel.{WireCreateRequest, WireSubscription}
import com.gu.newproduct.api.addsubscription.zuora.CreateSubscription.{SubscriptionName, ZuoraCreateSubRequest}
import com.gu.newproduct.api.addsubscription.zuora.GetAccount.WireModel.ZuoraAccount
import com.gu.newproduct.api.addsubscription.zuora.GetAccountSubscriptions.WireModel.ZuoraSubscriptionsResponse
import com.gu.newproduct.api.addsubscription.zuora.GetBillToContact.Contact
import com.gu.newproduct.api.addsubscription.zuora.GetBillToContact.WireModel.GetBillToResponse
import com.gu.newproduct.api.addsubscription.zuora.GetContacts.BilltoContact
import com.gu.newproduct.api.addsubscription.zuora.GetContacts.WireModel.GetContactsResponse
import com.gu.newproduct.api.addsubscription.zuora.GetPaymentMethod.{DirectDebit, PaymentMethod, PaymentMethodWire}
import com.gu.newproduct.api.addsubscription.zuora.{GetBillToContact, _}
import com.gu.newproduct.api.productcatalog.{DateRule, NewProductApi}
import com.gu.newproduct.api.addsubscription.zuora.{GetContacts, _}
import com.gu.newproduct.api.productcatalog.PlanId.MonthlyContribution
import com.gu.newproduct.api.productcatalog.{DateRule, NewProductApi, PlanId}
import com.gu.util.Logging
import com.gu.util.apigateway.ApiGatewayHandler.{LambdaIO, Operation}
import com.gu.util.apigateway.ResponseModels.ApiResponse
Expand All @@ -48,9 +51,9 @@ object Handler extends Logging {
}

object Steps {
def createZuoraSubRequest(request: AddSubscriptionRequest, acceptanceDate: LocalDate) = ZuoraCreateSubRequest(
def createZuoraSubRequest(request: AddSubscriptionRequest, acceptanceDate: LocalDate, amountMinorUnits: AmountMinorUnits) = ZuoraCreateSubRequest(
request.zuoraAccountId,
request.amountMinorUnits,
amountMinorUnits,
request.startDate,
acceptanceDate,
request.acquisitionCase,
Expand All @@ -68,37 +71,59 @@ object Steps {
currency: Currency,
paymentMethod: PaymentMethod,
firstPaymentDate: LocalDate,
billToContact: Contact
billToContact: BilltoContact,
amountMinorUnits: AmountMinorUnits
) =
ContributionsEmailData(
accountId = request.zuoraAccountId,
currency = currency,
paymentMethod = paymentMethod,
amountMinorUnits = request.amountMinorUnits,
amountMinorUnits = amountMinorUnits,
firstPaymentDate = firstPaymentDate,
billTo = billToContact
)

def addSubscriptionSteps(
getCustomerData: ZuoraAccountId => ApiGatewayOp[CustomerData],
validateRequest: (ValidatableFields, Currency) => ValidationResult[Unit],
def handleRequest(
addContribution: AddSubscriptionRequest => AsyncApiGatewayOp[SubscriptionName],
addVoucher: AddSubscriptionRequest => AsyncApiGatewayOp[SubscriptionName]
)(
apiGatewayRequest: ApiGatewayRequest
): Future[ApiResponse] = (for {
request <- apiGatewayRequest.bodyAsCaseClass[AddSubscriptionRequest]().withLogging("parsed request").toAsync
subscriptionName <- request.planId match {
case MonthlyContribution => addContribution(request)
case _ => addVoucher(request)
}
} yield ApiGatewayResponse(body = AddedSubscription(subscriptionName.value), statusCode = "200")).apiResponse

def addContributionSteps(
getCustomerData: ZuoraAccountId => ApiGatewayOp[ContributionCustomerData],
contributionValidations: (ValidatableFields, Currency) => ValidationResult[AmountMinorUnits],
createMonthlyContribution: ZuoraCreateSubRequest => ClientFailableOp[SubscriptionName],
sendConfirmationEmail: ContributionsEmailData => AsyncApiGatewayOp[Unit]
)(apiGatewayRequest: ApiGatewayRequest): Future[ApiResponse] = {
(for {
request <- apiGatewayRequest.bodyAsCaseClass[AddSubscriptionRequest]().withLogging("parsed request").toAsync
)(request: AddSubscriptionRequest): AsyncApiGatewayOp[SubscriptionName] = {
for {
customerData <- getCustomerData(request.zuoraAccountId).toAsync
CustomerData(account, paymentMethod, subscriptions, billTo) = customerData
ContributionCustomerData(account, paymentMethod, subscriptions, contacts) = customerData
validatableFields = ValidatableFields(request.amountMinorUnits, request.startDate)
_ <- validateRequest(validatableFields, account.currency).toApiGatewayOp.toAsync
amountMinorUnits <- contributionValidations(validatableFields, account.currency).toApiGatewayOp.toAsync
acceptanceDate = request.startDate.plusDays(paymentDelayFor(paymentMethod))
zuoraCreateSubRequest = createZuoraSubRequest(request, acceptanceDate)
zuoraCreateSubRequest = createZuoraSubRequest(request, acceptanceDate, amountMinorUnits)
subscriptionName <- createMonthlyContribution(zuoraCreateSubRequest).toAsyncApiGatewayOp("create monthly contribution")
contributionEmailData = toContributionEmailData(request, account.currency, paymentMethod, acceptanceDate, billTo)
contributionEmailData = toContributionEmailData(request, account.currency, paymentMethod, acceptanceDate, contacts.billTo, amountMinorUnits)
_ <- sendConfirmationEmail(contributionEmailData)
} yield ApiGatewayResponse(body = AddedSubscription(subscriptionName.value), statusCode = "200")).apiResponse
} yield subscriptionName
}

def addVoucherSteps(
getCustomerData: ZuoraAccountId => ApiGatewayOp[VoucherCustomerData],
validateStartDate: (PlanId, LocalDate) => ValidationResult[Unit]
)(request: AddSubscriptionRequest): AsyncApiGatewayOp[SubscriptionName] = for {
_ <- validateStartDate(request.planId, request.startDate).toApiGatewayOp.toAsync
customerData <- getCustomerData(request.zuoraAccountId).toAsync

} yield SubscriptionName("fakeSubId")

def operationForEffects(response: Request => Response, stage: Stage, fetchString: StringFromS3): ApiGatewayOp[Operation] =
for {
zuoraIds <- ZuoraIds.zuoraIdsForStage(stage)
Expand All @@ -111,43 +136,81 @@ object Steps {
contributionsSqsSend = EtSqsSend[ContributionFields](sqsSend) _
getCurrentDate = () => RawEffects.now().toLocalDate
validatorFor = DateValidator.validatorFor(getCurrentDate, _: DateRule)
isValidStartDate = StartDateValidator.fromRule(validatorFor, NewProductApi.catalog.monthlyContribution.startDateRules)

isValidStartDateForPlan = Function.uncurried(
NewProductApi.catalog.planForId andThen { plan =>
StartDateValidator.fromRule(validatorFor, plan.startDateRules)
}
)

createMonthlyContribution = CreateSubscription(zuoraIds.monthly, zuoraClient.post[WireCreateRequest, WireSubscription]) _
contributionIds = List(zuoraIds.monthly.productRatePlanId, zuoraIds.annual.productRatePlanId)
getCustomerData = getValidatedCustomerData(zuoraClient, contributionIds)
validateRequest = ValidateRequest(isValidStartDate, AmountLimits.limitsFor) _
getCustomerData = getValidatedContributionCustomerData(zuoraClient, contributionIds)
isValidContributionStartDate = isValidStartDateForPlan(MonthlyContribution, _: LocalDate)
validateRequest = ContributionValidations(isValidContributionStartDate, AmountLimits.limitsFor) _
sendConfirmationEmail = SendConfirmationEmail(contributionsSqsSend, getCurrentDate) _
contributionSteps = addContributionSteps(getCustomerData, validateRequest, createMonthlyContribution, sendConfirmationEmail) _

getVoucherData = getValidatedVoucherCustomerData(zuoraClient, contributionIds)
voucherSteps = addVoucherSteps(getVoucherData, isValidStartDateForPlan) _

addSubSteps = handleRequest(
addContribution = contributionSteps,
addVoucher = voucherSteps
) _

configuredOp = Operation.async(
steps = addSubscriptionSteps(getCustomerData, validateRequest, createMonthlyContribution, sendConfirmationEmail),
steps = addSubSteps,
healthcheck = () =>
HealthCheck(GetAccount(zuoraClient.get[ZuoraAccount]), AccountIdentitys.accountIdentitys(stage))
)
} yield configuredOp

def getValidatedCustomerData(
def getValidatedContributionCustomerData(
zuoraClient: Requests,
contributionPlanIds: List[ProductRatePlanId]
): ZuoraAccountId => ApiGatewayOp[CustomerData] = {
): ZuoraAccountId => ApiGatewayOp[ContributionCustomerData] = {

val validateAccount = ValidateAccount.apply _ thenValidate ContributionAccountValidation.apply _
val getValidatedAccount = GetAccount(zuoraClient.get[ZuoraAccount]) _ andValidateWith (
validate = ValidateAccount.apply _,
validate = validateAccount,
ifNotFoundReturn = Some("Zuora account id is not valid")
)
val getValidatedPaymentMethod = GetPaymentMethod(zuoraClient.get[PaymentMethodWire]) _ andValidateWith ValidatePaymentMethod.apply _
val validateSubs = ValidateSubscriptions(contributionPlanIds) _
val getValidatedSubs = GetAccountSubscriptions(zuoraClient.get[ZuoraSubscriptionsResponse]) _ andValidateWith validateSubs
val getBillTo = GetBillToContact(zuoraClient.get[GetBillToResponse]) _

GetCustomerData(
val getContactsFromZuora = GetContacts(zuoraClient.get[GetContactsResponse]) _
val getUnvalidatedContacts = getContactsFromZuora.andThen(_.toApiGatewayOp("getting contacts from Zuora"))
GetContributionCustomerData(
getAccount = getValidatedAccount,
getPaymentMethod = getValidatedPaymentMethod,
getBilltoContact = getBillTo,
getContacts = getUnvalidatedContacts,
getAccountSubscriptions = getValidatedSubs,
_
)
}

def getValidatedVoucherCustomerData(
zuoraClient: Requests,
contributionPlanIds: List[ProductRatePlanId]
): ZuoraAccountId => ApiGatewayOp[VoucherCustomerData] = {

val validateAccount = ValidateAccount.apply _ thenValidate VoucherAccountValidation.apply _
val getValidatedAccount = GetAccount(zuoraClient.get[ZuoraAccount]) _ andValidateWith (
validate = validateAccount,
ifNotFoundReturn = Some("Zuora account id is not valid")
)
val getValidatedPaymentMethod = GetPaymentMethod(zuoraClient.get[PaymentMethodWire]) _ andValidateWith ValidatePaymentMethod.apply _
val getContacts = GetContacts(zuoraClient.get[GetContactsResponse]) _
val getValidatedContacts = getContacts andValidateWith (ValidateContactsForVoucher.apply _)
GetVoucherCustomerData(
getAccount = getValidatedAccount,
getPaymentMethod = getValidatedPaymentMethod,
getContacts = getValidatedContacts,
_
)
}

def emailQueueFor(stage: Stage) = stage match {
case Stage("PROD") => QueueName("contributions-thanks")
case Stage("CODE") => QueueName("contributions-thanks")
Expand Down
Expand Up @@ -6,7 +6,7 @@ import com.gu.util.apigateway.ResponseModels.ApiResponse
import com.gu.util.reader.AsyncTypes._
import com.gu.util.reader.Types.ApiGatewayOp.{ContinueProcessing, ReturnWithResponse}
import com.gu.util.reader.Types._
import com.gu.util.resthttp.Types.{ClientFailableOp, NotFound}
import com.gu.util.resthttp.Types.{ClientFailableOp, ClientSuccess, GenericError, NotFound}

import scala.concurrent.Future

Expand Down Expand Up @@ -41,4 +41,11 @@ object TypeConvert {
}
}

implicit class OptionToClientFailableOp[A](option: Option[A]) {
def toClientFailable(errorMessage: String) = option match {
case None => GenericError(errorMessage)
case Some(value) => ClientSuccess(value)
}
}

}
Expand Up @@ -33,8 +33,17 @@ object ZuoraIds {
productRatePlanId = ProductRatePlanId("2c92c0f95e1d5c9c015e38f8c87d19a1"),
productRatePlanChargeId = ProductRatePlanChargeId("2c92c0f95e1d5c9c015e38f8c8ac19a3")
)
),
Stage("DEV") -> ContributionsZuoraIds(
monthly = PlanAndCharge(
productRatePlanId = ProductRatePlanId("2c92c0f85a6b134e015a7fcd9f0c7855"),
productRatePlanChargeId = ProductRatePlanChargeId("2c92c0f85a6b1352015a7fcf35ab397c")
),
annual = PlanAndCharge(
productRatePlanId = ProductRatePlanId("2c92c0f85e2d19af015e3896e824092c"),
productRatePlanChargeId = ProductRatePlanChargeId("2c92c0f85e2d19af015e3896e84d092e")
)
)
// probably don't need dev as we'd just pass in the actual object in the test
)
mappings.get(stage).toApiGatewayContinueProcessing(ApiGatewayResponse.internalServerError(s"missing zuora ids for stage $stage"))
}
Expand Down
Expand Up @@ -4,7 +4,7 @@ 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.GetContacts.BilltoContact
import com.gu.newproduct.api.addsubscription.zuora.GetPaymentMethod.{DirectDebit, PaymentMethod}
import com.gu.newproduct.api.addsubscription.{AmountMinorUnits, ZuoraAccountId}
import com.gu.util.Logging
Expand All @@ -22,7 +22,7 @@ object SendConfirmationEmail extends Logging {
paymentMethod: PaymentMethod,
amountMinorUnits: AmountMinorUnits,
firstPaymentDate: LocalDate,
billTo: Contact
billTo: BilltoContact
)

def apply(
Expand Down
Expand Up @@ -5,7 +5,7 @@ import com.gu.newproduct.api.addsubscription.validation.Validation._
import com.gu.newproduct.api.addsubscription.zuora.GetAccount._

case class ValidatedAccount(
identityId: IdentityId,
identityId: Option[IdentityId],
paymentMethodId: PaymentMethodId,
autoPay: AutoPay,
accountBalanceMinorUnits: AccountBalanceMinorUnits,
Expand All @@ -15,12 +15,11 @@ case class ValidatedAccount(
object ValidateAccount {
def apply(account: Account): ValidationResult[ValidatedAccount] = {
for {
identityId <- account.identityId getOrFailWith "Zuora account has no Identity Id"
_ <- account.autoPay.value orFailWith "Zuora account has autopay disabled"
_ <- (account.accountBalanceMinorUnits.value == 0) orFailWith "Zuora account balance is not zero"
paymentMethodId <- account.paymentMethodId getOrFailWith "Zuora account has no default payment method"
} yield ValidatedAccount(
identityId,
account.identityId,
paymentMethodId,
account.autoPay,
account.accountBalanceMinorUnits,
Expand Down

This file was deleted.

Expand Up @@ -18,7 +18,7 @@ object Validation {
}
}

implicit class ComposeValidation[ID, DATA](getter: ID => ClientFailableOp[DATA]) {
implicit class GetAndValidate[ID, DATA](getter: ID => ClientFailableOp[DATA]) {
def andValidateWith[VALIDATED](validate: DATA => ValidationResult[VALIDATED], ifNotFoundReturn: Option[String] = None): ID => ApiGatewayOp[VALIDATED] =
(id: ID) =>
for {
Expand All @@ -30,5 +30,14 @@ object Validation {
} yield validatedData
}

implicit class ComposeValidation[UNVALIDATED, VALIDATED](initialValidation: UNVALIDATED => ValidationResult[VALIDATED]) {
def thenValidate[TWICEVALIDATED](finalValidation: VALIDATED => ValidationResult[TWICEVALIDATED]): UNVALIDATED => ValidationResult[TWICEVALIDATED] =
(unvalidated: UNVALIDATED) =>
for {
validated <- initialValidation(unvalidated)
twiceValidated <- finalValidation(validated)
} yield twiceValidated
}

}

@@ -0,0 +1,14 @@
package com.gu.newproduct.api.addsubscription.validation.contribution

import com.gu.newproduct.api.addsubscription.validation.{Failed, Passed, ValidatedAccount, ValidationResult}

object ContributionAccountValidation {
def apply(
account: ValidatedAccount
): ValidationResult[ValidatedAccount] = {
account.identityId match {
case Some(_) => Passed(account)
case None => Failed("Zuora account has no Identity Id")
}
}
}

0 comments on commit 40567c2

Please sign in to comment.