Skip to content

Commit

Permalink
Merge pull request #406 from guardian/add-price-to-potential-holiday-…
Browse files Browse the repository at this point in the history
…stop-endpoint

Add price to potential holiday stop endpoint
  • Loading branch information
frj committed Sep 6, 2019
2 parents 867ef7e + 0a0b7a5 commit dad20ef
Show file tree
Hide file tree
Showing 17 changed files with 442 additions and 144 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ lazy val `sf-gocardless-sync` = all(project in file("handlers/sf-gocardless-sync

lazy val `holiday-stop-api` = all(project in file("handlers/holiday-stop-api"))
.enablePlugins(RiffRaffArtifact)
.dependsOn(`holiday-stops`, handler, effectsDepIncludingTestFolder, testDep)
.dependsOn(`holiday-stops` % "compile->compile;test->test", handler, effectsDepIncludingTestFolder, testDep)

lazy val `sf-datalake-export` = all(project in file("handlers/sf-datalake-export"))
.enablePlugins(RiffRaffArtifact)
Expand Down
7 changes: 4 additions & 3 deletions handlers/holiday-stop-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ All endpoints require...

| Method | Endpoint | Description |
| --- | --- | --- |
| GET | `/{STAGE}/potential?startDate={yyyy-MM-dd}&endDate={yyyy-MM-dd}` (with `x-product-name-prefix` header set) | returns an array of dates for each issue impacted between the dates for the given product |
| GET | `/{STAGE}/potential?startDate={yyyy-MM-dd}&endDate={yyyy-MM-dd}` (with `x-product-name-prefix` header set) | [DEPRECATED]returns an array of dates for each issue impacted between the dates for the given product |
| GET | `/{STAGE}/potential/{SUBSCRIPTION_NAME}?startDate={yyyy-MM-dd}&endDate={yyyy-MM-dd}&estimateCredit={true`|`false}` (with `x-product-name-prefix` header set) | returns a response containing dates for each issue impacted between the dates for the given product. Optionally the estimated credit can be calculated for each issue. |
| GET | `/{STAGE}/hsr` | returns all holiday stops (past & present) for the user |
| GET | `/{STAGE}/hsr` (with `x-product-name-prefix` header set) | returns all holiday stops (past & present) but including a calculated 'first available date' based on the type of product |
| GET | `/{STAGE}/hsr /{SUBSCRIPTION_NAME}` (with `x-product-name-prefix` header set) | returns all holiday stops (past & present) for the user filtered on the specified subscription also including a calculated 'first available date' based on the type of product |
| GET | `/{STAGE}/hsr/{SUBSCRIPTION_NAME}` (with `x-product-name-prefix` header set) | returns all holiday stops (past & present) for the user filtered on the specified subscription also including a calculated 'first available date' based on the type of product |
| POST | `/{STAGE}/hsr` | creates a new all holiday stop, example body `{ "start": "2023-06-10", "end": "2024-06-14", "subscriptionName": "A-S00071783" }`|
| DELETE | `/{STAGE}/hsr /{SUBSCRIPTION_NAME} /{SF_ID}` | deletes the holiday stop request from SalesForce (with Id matching `{SF_ID}`) |
| DELETE | `/{STAGE}/hsr/{SUBSCRIPTION_NAME} /{SF_ID}` | deletes the holiday stop request from SalesForce (with Id matching `{SF_ID}`) |


### Handling Multiple Environments
Expand Down
29 changes: 29 additions & 0 deletions handlers/holiday-stop-api/cfn.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,35 @@ Resources:
- HolidayStopApiLambda
- HolidayStopApiPotentialProxyResource

HolidayStopApiPotentialBySubscriptionNameProxyResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref HolidayStopApi
ParentId: !Ref HolidayStopApiPotentialProxyResource
PathPart: "{subscriptionName}"
DependsOn:
- HolidayStopApi
- HolidayStopApiPotentialProxyResource

HolidayStopApiPotentialBySubscriptionNameGetMethod:
Type: AWS::ApiGateway::Method
Properties:
AuthorizationType: NONE
ApiKeyRequired: true
RestApiId: !Ref HolidayStopApi
ResourceId: !Ref HolidayStopApiPotentialBySubscriptionNameProxyResource
HttpMethod: GET
RequestParameters:
method.request.path.subscriptionName: true
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST # this for the interaction between API Gateway and Lambda and MUST be POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HolidayStopApiLambda.Arn}/invocations
DependsOn:
- HolidayStopApi
- HolidayStopApiLambda
- HolidayStopApiPotentialBySubscriptionNameProxyResource

HolidayStopApiGetAllAndCreateProxyResource:
Type: AWS::ApiGateway::Resource
Properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import java.time.LocalDate

import com.amazonaws.services.lambda.runtime.Context
import com.gu.effects.{GetFromS3, RawEffects}
import com.gu.salesforce.SalesforceAuthenticate.SFAuthConfig
import com.gu.salesforce.SalesforceClient
import com.gu.salesforce.holiday_stops.SalesforceHolidayStopRequest._
import com.gu.salesforce.holiday_stops.SalesforceHolidayStopRequestsDetail.{HolidayStopRequestId, ProductName, SubscriptionName}
Expand All @@ -22,71 +21,105 @@ import com.gu.util.reader.Types._
import com.gu.util.resthttp.JsonHttp.StringHttpRequest
import com.gu.util.resthttp.RestRequestMaker.BodyAsString
import com.gu.util.resthttp.{HttpOp, JsonHttp}
import com.softwaremill.sttp.{HttpURLConnectionBackend, Id, SttpBackend}
import okhttp3.{Request, Response}
import play.api.libs.json.{Format, Json, Reads}
import play.api.libs.json.{Json, Reads}
import scalaz.{-\/, \/, \/-}

object Handler extends Logging {

type SfClient = HttpOp[StringHttpRequest, BodyAsString]

def apply(inputStream: InputStream, outputStream: OutputStream, context: Context): Unit =
def apply(inputStream: InputStream, outputStream: OutputStream, context: Context): Unit = {
ApiGatewayHandler(
LambdaIO(
inputStream,
outputStream,
context
)
)(
operationForEffects(
RawEffects.response,
RawEffects.stage,
GetFromS3.fetchString
operationForEffects(
RawEffects.response,
RawEffects.stage,
GetFromS3.fetchString,
HttpURLConnectionBackend()
)
)
)

val POTENTIAL_PROXY_RESOURCE_PATH = "/potential"
val GET_ALL_AND_CREATE_PROXY_RESOURCE_REGEX = """/hsr.*""".r
}

def operationForEffects(
response: Request => Response,
stage: Stage,
fetchString: StringFromS3
fetchString: StringFromS3,
backend: SttpBackend[Id, Nothing]
): ApiGatewayOp[Operation] = {

val loadConfig = LoadConfigModule(stage, fetchString)

for {
sfAuthConfig <- loadConfig[SFAuthConfig].toApiGatewayOp("load sfAuth config")
sfClient <- SalesforceClient(response, sfAuthConfig).value.toDisjunction.toApiGatewayOp("authenticate with SalesForce")
} yield Operation.noHealthcheck( // checking connectivity to SF is sufficient healthcheck so no special steps required
request => validateRequestAndCreateSteps(request)(request, sfClient)
)
config <- Config(fetchString).toApiGatewayOp("Failed to load config")
sfClient <- SalesforceClient(response, config.sfConfig).value.toDisjunction.toApiGatewayOp("authenticate with SalesForce")
} yield Operation.noHealthcheck(request => // checking connectivity to SF is sufficient healthcheck so no special steps required
validateRequestAndCreateSteps(request, config, backend)(request, sfClient))
}

private def validateRequestAndCreateSteps(request: ApiGatewayRequest) = {
private def validateRequestAndCreateSteps(
request: ApiGatewayRequest,
config: Config,
backend: SttpBackend[Id, Nothing]
) = {
(for {
httpMethod <- validateMethod(request.httpMethod)
path <- validatePath(request.path)
} yield createSteps(httpMethod, path)).fold(
} yield createSteps(httpMethod, splitPath(path), config, backend)).fold(
{ errorMessage: String =>
badrequest(errorMessage) _
},
identity
)
}

private def createSteps(httpMethod: String, path: String) = {
private def validateMethod(method: Option[String]): String \/ String = {
method match {
case Some(method) => \/-(method)
case None => -\/("Http method is required")
}
}

private def validatePath(path: Option[String]): String \/ String = {
path match {
case Some(method) => \/-(method)
case None => -\/("Path is required")
}
}

private def createSteps(
httpMethod: String,
path: List[String],
config: Config,
backend: SttpBackend[Id, Nothing]
) = {
path match {
case POTENTIAL_PROXY_RESOURCE_PATH =>
case "potential" :: Nil =>
httpMethod match {
case "GET" => stepsForPotentialHolidayStop _
case "GET" => stepsForPotentialHolidayStopV1 _
case _ => unsupported _
}
case GET_ALL_AND_CREATE_PROXY_RESOURCE_REGEX() =>
case "potential" :: _ :: Nil =>
httpMethod match {
case "GET" => stepsForPotentialHolidayStopV2(config, backend) _
case _ => unsupported _
}
case "hsr" :: Nil =>
httpMethod match {
case "GET" => stepsToListExisting _
case "POST" => stepsToCreate _
case _ => unsupported _
}
case "hsr" :: _ :: Nil =>
httpMethod match {
case "GET" => stepsToListExisting _
case _ => unsupported _
}
case "hsr" :: _ :: _ :: Nil =>
httpMethod match {
case "DELETE" => stepsToDelete _
case _ => unsupported _
}
Expand All @@ -95,17 +128,10 @@ object Handler extends Logging {
}
}

private def validateMethod(method: Option[String]): String \/ String = {
method match {
case Some(method) => \/-(method)
case None => -\/("Http method is required")
}
}

private def validatePath(path: Option[String]): String \/ String = {
path match {
case Some(method) => \/-(method)
case None => -\/("Path is required")
def splitPath(pathString: String): List[String] = {
pathString.split('/').toList match {
case "" :: tail => tail
case noLeadingSlash => noLeadingSlash
}
}

Expand All @@ -118,28 +144,63 @@ object Handler extends Logging {
case (HEADER_IDENTITY_ID, identityId) => Left(IdentityId(identityId))
}).toApiGatewayOp(s"either '$HEADER_IDENTITY_ID' header OR '$HEADER_SALESFORCE_CONTACT_ID' (one is required)")

case class PotentialHolidayStopParams(startDate: LocalDate, endDate: LocalDate)
case class PotentialHolidayStopsV1PathParams(startDate: LocalDate, endDate: LocalDate)

def stepsForPotentialHolidayStop(req: ApiGatewayRequest, unused: SfClient): ApiResponse = {
implicit val formatLocalDateAsSalesforceDate: Format[LocalDate] = SalesforceHolidayStopRequest.formatLocalDateAsSalesforceDate
implicit val readsPotentialHolidayStopParams: Reads[PotentialHolidayStopParams] = Json.reads[PotentialHolidayStopParams]
def stepsForPotentialHolidayStopV1(req: ApiGatewayRequest, unused: SfClient): ApiResponse = {
implicit val formatLocalDateAsSalesforceDate = SalesforceHolidayStopRequest.formatLocalDateAsSalesforceDate
implicit val readsPotentialHolidayStopParams = Json.reads[PotentialHolidayStopsV1PathParams]
(for {
productNamePrefix <- req.headers.flatMap(_.get(HEADER_PRODUCT_NAME_PREFIX)).toApiGatewayOp("identityID header")
params <- req.queryParamsAsCaseClass[PotentialHolidayStopParams]()
productNamePrefix <- req.headers.flatMap(_.get(HEADER_PRODUCT_NAME_PREFIX)).toApiGatewayOp(s"missing '$HEADER_PRODUCT_NAME_PREFIX' header")
params <- req.queryParamsAsCaseClass[PotentialHolidayStopsV1PathParams]()
} yield ApiGatewayResponse(
"200",
ActionCalculator.publicationDatesToBeStopped(params.startDate, params.endDate, ProductName(productNamePrefix))
)).apiResponse
}

case class GetPathParams(subscriptionName: Option[SubscriptionName])
case class PotentialHolidayStopsV2PathParams(subscriptionName: SubscriptionName)
case class PotentialHolidayStopsV2QueryParams(startDate: LocalDate, endDate: LocalDate, estimateCredit: Option[String])

def stepsForPotentialHolidayStopV2(config: Config, backend: SttpBackend[Id, Nothing])(req: ApiGatewayRequest, unused: SfClient): ApiResponse = {
implicit val reads: Reads[PotentialHolidayStopsV2QueryParams] = Json.reads[PotentialHolidayStopsV2QueryParams]
(for {
pathParams <- req.pathParamsAsCaseClass[PotentialHolidayStopsV2PathParams]()(Json.reads[PotentialHolidayStopsV2PathParams])
productNamePrefix <- req.headers.flatMap(_.get(HEADER_PRODUCT_NAME_PREFIX)).toApiGatewayOp(s"missing '$HEADER_PRODUCT_NAME_PREFIX' header")
queryParams <- req.queryParamsAsCaseClass[PotentialHolidayStopsV2QueryParams]()
credit <- estimateCredit(queryParams, pathParams, config, backend)
} yield ApiGatewayResponse(
"200",
PotentialHolidayStopsResponse(
ActionCalculator.publicationDatesToBeStopped(queryParams.startDate, queryParams.endDate, ProductName(productNamePrefix))
.map(PotentialHolidayStop(_, credit))
)
)).apiResponse
}

def estimateCredit(
queryParams: PotentialHolidayStopsV2QueryParams,
pathParams: PotentialHolidayStopsV2PathParams,
config: Config,
backend: SttpBackend[Id, Nothing]
): ApiGatewayOp[Option[Double]] = {
if (queryParams.estimateCredit == Some("true")) {
CreditCalculator
.guardianWeeklyCredit(config, pathParams.subscriptionName, backend)
.toApiGatewayOp("Failed to calculate credit")
.map(Some(_))
} else {
ContinueProcessing(None)
}
}

case class ListExistingPathParams(subscriptionName: Option[SubscriptionName])

def stepsToListExisting(req: ApiGatewayRequest, sfClient: SfClient): ApiResponse = {

val lookupOp = SalesforceHolidayStopRequest.LookupByContactAndOptionalSubscriptionName(sfClient.wrapWith(JsonHttp.getWithParams))

val extractOptionalSubNameOp: ApiGatewayOp[Option[SubscriptionName]] = req.pathParameters match {
case Some(_) => req.pathParamsAsCaseClass[GetPathParams]()(Json.reads[GetPathParams]).map(_.subscriptionName)
case Some(_) => req.pathParamsAsCaseClass[ListExistingPathParams]()(Json.reads[ListExistingPathParams]).map(_.subscriptionName)
case None => ContinueProcessing(None)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.gu.holiday_stops

import java.time.LocalDate

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

case class PotentialHolidayStop(publicationDate: LocalDate, credit: Option[Double])

object PotentialHolidayStop {
implicit val reads: Format[PotentialHolidayStop] = Json.format[PotentialHolidayStop]
}

case class PotentialHolidayStopsResponse(potentials: List[PotentialHolidayStop])

object PotentialHolidayStopsResponse {
implicit val reads: Format[PotentialHolidayStopsResponse] = Json.format[PotentialHolidayStopsResponse]
}
Loading

0 comments on commit dad20ef

Please sign in to comment.