Skip to content

Commit

Permalink
Use query parameters when loading checkout page
Browse files Browse the repository at this point in the history
  • Loading branch information
George Schneeloch committed May 16, 2019
1 parent fb796eb commit cfeb6ce
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 19 deletions.
55 changes: 42 additions & 13 deletions static/js/containers/pages/CheckoutPage.js
Expand Up @@ -2,19 +2,22 @@
import React from "react"
import * as R from "ramda"
import { connect } from "react-redux"
import { connectRequest, mutateAsync } from "redux-query"
import { mutateAsync, requestAsync } from "redux-query"
import { compose } from "redux"
import queryString from "query-string"

import queries from "../../lib/queries"
import {
calculateDiscount,
calculatePrice,
formatPrice,
formatRunTitle
formatRunTitle,
formatErrors
} from "../../lib/ecommerce"
import { createCyberSourceForm } from "../../lib/form"

import type { Response } from "redux-query"
import type { Location } from "react-router"
import type {
BasketResponse,
BasketPayload,
Expand Down Expand Up @@ -50,6 +53,8 @@ export const calcSelectedRunIds = (item: BasketItem): { [number]: number } => {
type Props = {
basket: ?BasketResponse,
checkout: () => Promise<Response<CheckoutResponse>>,
fetchBasket: () => Promise<*>,
location: Location,
updateBasket: (payload: BasketPayload) => Promise<*>
}
type State = {
Expand All @@ -64,6 +69,30 @@ export class CheckoutPage extends React.Component<Props, State> {
errors: null
}

componentDidMount = async () => {
const {
fetchBasket,
location: { search }
} = this.props
const params = queryString.parse(search)
const productId = parseInt(params.product)
if (!productId) {
await fetchBasket()
return
}

try {
await this.updateBasket({ items: [{ id: productId }] })

const couponCode = params.code
if (couponCode) {
await this.updateBasket({ coupons: [{ code: couponCode }] })
}
} catch (_) {
// prevent complaints about unresolved promises
}
}

handleErrors = async (responsePromise: Promise<*>) => {
const response = await responsePromise
if (response.body.errors) {
Expand Down Expand Up @@ -202,13 +231,14 @@ export class CheckoutPage extends React.Component<Props, State> {
const { basket } = this.props
const { couponCode, errors } = this.state

if (!basket) {
return null
}

const item = basket.items[0]
if (!item) {
return <div>No item in basket</div>
const item = basket && basket.items[0]
if (!basket || !item) {
return (
<div className="checkout-page">
No item in basket
{formatErrors(errors)}
</div>
)
}

const coupon = basket.coupons.find(coupon =>
Expand Down Expand Up @@ -259,7 +289,7 @@ export class CheckoutPage extends React.Component<Props, State> {
Apply
</button>
</div>
{errors ? <div className="error">Error: {errors}</div> : null}
{formatErrors(errors)}
</form>
</div>
</div>
Expand Down Expand Up @@ -302,15 +332,14 @@ const mapStateToProps = state => ({
})
const mapDispatchToProps = dispatch => ({
checkout: () => dispatch(mutateAsync(queries.ecommerce.checkoutMutation())),
fetchBasket: () => dispatch(requestAsync(queries.ecommerce.basketQuery())),
updateBasket: payload =>
dispatch(mutateAsync(queries.ecommerce.basketMutation(payload)))
})
const mapPropsToConfigs = () => [queries.ecommerce.basketQuery()]

export default compose(
connect(
mapStateToProps,
mapDispatchToProps
),
connectRequest(mapPropsToConfigs)
)
)(CheckoutPage)
137 changes: 131 additions & 6 deletions static/js/containers/pages/CheckoutPage_test.js
Expand Up @@ -18,7 +18,7 @@ import {
formatPrice,
formatRunTitle
} from "../../lib/ecommerce"
import { assertRaises } from "../../lib/util"
import { assertRaises, wait } from "../../lib/util"
import { PRODUCT_TYPE_COURSERUN, PRODUCT_TYPE_PROGRAM } from "../../constants"

describe("CheckoutPage", () => {
Expand All @@ -36,7 +36,9 @@ describe("CheckoutPage", () => {
basket
}
},
{}
{
location: {}
}
)
})

Expand Down Expand Up @@ -102,6 +104,132 @@ describe("CheckoutPage", () => {
assert.equal(inner.find("img").prop("alt"), basketItem.description)
assert.equal(inner.find(".item-row .title").text(), basketItem.description)
})
;[true, false].forEach(hasError => {
it(`updates the basket with a product id from the query parameter${
hasError ? ", but an error is returned" : ""
}`, async () => {
const productId = 4567
if (hasError) {
helper.handleRequestStub.withArgs("/api/basket/", "PATCH").returns({
status: 400,
body: {
errors: "error"
}
})
}
const { inner } = await renderPage(
{},
{
location: {
search: `product=${productId}`
}
}
)

sinon.assert.calledWith(
helper.handleRequestStub,
"/api/basket/",
"PATCH",
{
body: { items: [{ id: productId }] },
credentials: undefined,
headers: {
"X-CSRFTOKEN": null
}
}
)
assert.equal(inner.state().errors, hasError ? "error" : null)
})
})

//
;[[true, false], [true, true], [false, false], [false, true]].forEach(
([hasValidProductId, hasValidCoupon]) => {
it.only(`updates the basket with a ${
hasValidProductId ? "" : "in"
}valid product id and a ${
hasValidCoupon ? "" : "in"
}valid coupon code from the query parameter`, async () => {
const couponCode = "codeforcoupon"
const productId = 12345
const couponPayload = {
body: { coupons: [{ code: couponCode }] },
credentials: undefined,
headers: {
"X-CSRFTOKEN": null
}
}
const productPayload = {
body: { items: [{ id: productId }] },
credentials: undefined,
headers: {
"X-CSRFTOKEN": null
}
}

if (!hasValidProductId) {
helper.handleRequestStub
.withArgs("/api/basket/", "PATCH", productPayload)
.returns({
status: 400,
body: {
errors: "product error"
}
})
}
if (!hasValidCoupon) {
helper.handleRequestStub
.withArgs("/api/basket/", "PATCH", couponPayload)
.returns({
status: 400,
body: {
errors: "coupon error"
}
})
}
const { inner } = await renderPage(
{},
{
location: {
search: `product=${productId}&code=${couponCode}`
}
}
)
// wait for componentDidMount to resolve
await wait(15)
sinon.assert.calledWith(
helper.handleRequestStub,
"/api/basket/",
"PATCH",
productPayload
)
if (hasValidProductId) {
sinon.assert.calledWith(
helper.handleRequestStub,
"/api/basket/",
"PATCH",
couponPayload
)
} else {
sinon.assert.neverCalledWith(
helper.handleRequestStub,
"/api/basket/",
"PATCH",
couponPayload
)
}

assert.equal(
inner.state().errors,
!hasValidProductId
? "product error"
: !hasValidCoupon
? "coupon error"
: null
)
})
}
)

it("displays the coupon code", async () => {
const { inner } = await renderPage()
Expand Down Expand Up @@ -167,10 +295,7 @@ describe("CheckoutPage", () => {
})

assert.equal(inner.state().errors, errors)
assert.equal(
inner.find(".enrollment-input .error").text(),
"Error: Unknown error"
)
assert.equal(inner.find(".enrollment-input .error").text(), "Unknown error")
assert.isTrue(inner.find(".enrollment-input input.error-border").exists())
})

Expand Down
19 changes: 19 additions & 0 deletions static/js/lib/ecommerce.js
@@ -1,4 +1,5 @@
// @flow
import React from "react"
import Decimal from "decimal.js-light"
import * as R from "ramda"
import { equals } from "ramda"
Expand Down Expand Up @@ -57,6 +58,24 @@ const formatDateForRun = (dateString: ?string) =>
export const formatRunTitle = (run: CourseRun) =>
`${formatDateForRun(run.start_date)} - ${formatDateForRun(run.end_date)}`

export const formatErrors = (errors: string | Object) => {
if (!errors) {
return null
}

let errorString
if (typeof errors === "object") {
if (errors.items) {
errorString = errors.items[0]
} else {
errorString = errors[0]
}
} else {
errorString = errors
}
return <div className="error">{errorString}</div>
}

export const isPromo = equals(COUPON_TYPE_PROMO)

export const createProductMap = (
Expand Down

0 comments on commit cfeb6ce

Please sign in to comment.