Merge pull request #684 from sharetribe/refactor-booking-flow-for-units

Refactor booking flow for units
This commit is contained in:
Vesa Luusua 2018-02-02 16:39:00 +02:00 committed by GitHub
commit a2a6e62036
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 409 additions and 267 deletions

View file

@ -3,255 +3,43 @@
* I.e. dates and other details related to payment decision in receipt format.
*/
import React from 'react';
import { bool, oneOf, string } from 'prop-types';
import { FormattedMessage, FormattedHTMLMessage, intlShape, injectIntl } from 'react-intl';
import moment from 'moment';
import { oneOf, string } from 'prop-types';
import { intlShape, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { types as sdkTypes } from '../../util/sdkLoader';
import { formatMoney } from '../../util/currency';
import {
LINE_ITEM_NIGHT,
LINE_ITEM_PROVIDER_COMMISSION,
txIsCanceled,
txIsCompleted,
txIsDeclinedOrExpired,
propTypes,
} from '../../util/types';
import { daysBetween } from '../../util/dates';
import { propTypes } from '../../util/types';
import LineItemUnitPrice from './LineItemUnitPrice';
import LineItemBookingPeriod from './LineItemBookingPeriod';
import LineItemUnitsMaybe from './LineItemUnitsMaybe';
import LineItemSubTotalMaybe from './LineItemSubTotalMaybe';
import LineItemCommissionMaybe from './LineItemCommissionMaybe';
import LineItemRefundMaybe from './LineItemRefundMaybe';
import LineItemTotalPrice from './LineItemTotalPrice';
import css from './BookingBreakdown.css';
const { Money } = sdkTypes;
// Validate the assumption that the commission exists and the amount
// is zero or negative.
const isValidCommission = commissionLineItem => {
return (
commissionLineItem &&
commissionLineItem.lineTotal instanceof Money &&
commissionLineItem.lineTotal.amount <= 0
);
};
const UnitPriceItem = props => {
const { transaction, unitType, intl } = props;
const isNightly = unitType === LINE_ITEM_NIGHT;
const unitPurchase = transaction.attributes.lineItems.find(
item => item.code === unitType && !item.reversal
);
const formattedUnitPrice = formatMoney(intl, unitPurchase.unitPrice);
return (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage
id={isNightly ? 'BookingBreakdown.pricePerNight' : 'BookingBreakdown.pricePerDay'}
/>
</span>
<span className={css.itemValue}>{formattedUnitPrice}</span>
</div>
);
};
UnitPriceItem.propTypes = {
transaction: propTypes.transaction.isRequired,
unitType: propTypes.bookingUnitType.isRequired,
intl: intlShape.isRequired,
};
const UnitsItem = props => {
const { transaction, booking, unitType, intl } = props;
const { start: startDate, end: endDateRaw } = booking.attributes;
const isNightly = unitType === LINE_ITEM_NIGHT;
const isSingleDay = !isNightly && daysBetween(startDate, endDateRaw) === 1;
const endDay = isNightly ? endDateRaw : moment(endDateRaw).subtract(1, 'days');
const dateFormatOptions = {
weekday: 'short',
month: 'short',
day: 'numeric',
};
const bookingPeriod = isSingleDay ? (
intl.formatDate(startDate, dateFormatOptions)
) : (
<FormattedMessage
id="BookingBreakdown.bookingPeriod"
values={{
bookingStart: (
<span className={css.nowrap}>{intl.formatDate(startDate, dateFormatOptions)}</span>
),
bookingEnd: (
<span className={css.nowrap}>{intl.formatDate(endDay, dateFormatOptions)}</span>
),
}}
/>
);
const unitPurchase = transaction.attributes.lineItems.find(
item => item.code === unitType && !item.reversal
);
const unitCount = unitPurchase.quantity.toFixed();
const unitCountMessage = (
<FormattedHTMLMessage
id={isNightly ? 'BookingBreakdown.nightCount' : 'BookingBreakdown.dayCount'}
values={{ count: unitCount }}
/>
);
return (
<div className={css.lineItem}>
<span className={css.itemLabel}>{bookingPeriod}</span>
<span className={css.itemValue}>{unitCountMessage}</span>
</div>
);
};
UnitsItem.propTypes = {
transaction: propTypes.transaction.isRequired,
booking: propTypes.booking.isRequired,
intl: intlShape.isRequired,
};
const SubTotalItemMaybe = props => {
const { transaction, unitType, isProvider, intl } = props;
const refund = transaction.attributes.lineItems.find(
item => item.code === unitType && item.reversal
);
// Show unit purchase line total (unit price * quantity) as a subtotal.
// PLEASE NOTE that this assumes that the transaction doesn't have other
// line item types than the defined unit type (e.g. week, month, year).
const showSubTotal = isProvider || refund;
const unitPurchase = transaction.attributes.lineItems.find(
item => item.code === unitType && !item.reversal
);
const formattedSubTotal = formatMoney(intl, unitPurchase.lineTotal);
return showSubTotal ? (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.subTotal" />
</span>
<span className={css.itemValue}>{formattedSubTotal}</span>
</div>
) : null;
};
SubTotalItemMaybe.propTypes = {
transaction: propTypes.transaction.isRequired,
isProvider: bool.isRequired,
intl: intlShape.isRequired,
};
const CommissionItemMaybe = props => {
const { transaction, isProvider, intl } = props;
const providerCommissionLineItem = transaction.attributes.lineItems.find(
item => item.code === LINE_ITEM_PROVIDER_COMMISSION && !item.reversal
);
const commissionRefund = transaction.attributes.lineItems.find(
item => item.code === LINE_ITEM_PROVIDER_COMMISSION && item.reversal
);
// If commission is passed it will be shown as a fee already reduces from the total price
let commissionItem = null;
if (isProvider) {
if (!isValidCommission(providerCommissionLineItem)) {
// eslint-disable-next-line no-console
console.error('invalid commission line item:', providerCommissionLineItem);
throw new Error('Commission should be present and the value should be zero or negative');
}
const commission = providerCommissionLineItem.lineTotal;
const formattedCommission = commission ? formatMoney(intl, commission) : null;
commissionItem = commissionRefund ? null : (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.commission" />
</span>
<span className={css.itemValue}>{formattedCommission}</span>
</div>
);
}
return commissionItem;
};
CommissionItemMaybe.propTypes = {
transaction: propTypes.transaction.isRequired,
isProvider: bool.isRequired,
intl: intlShape.isRequired,
};
const RefundItemMaybe = props => {
const { transaction, unitType, intl } = props;
const refund = transaction.attributes.lineItems.find(
item => item.code === unitType && item.reversal
);
return refund ? (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.refund" />
</span>
<span className={css.itemValue}>{formatMoney(intl, refund.lineTotal)}</span>
</div>
) : null;
};
RefundItemMaybe.propTypes = {
transaction: propTypes.transaction.isRequired,
intl: intlShape.isRequired,
};
export const BookingBreakdownComponent = props => {
const { rootClassName, className, userRole, unitType, transaction, booking, intl } = props;
const isProvider = userRole === 'provider';
const classes = classNames(rootClassName || css.root, className);
let providerTotalMessageId = 'BookingBreakdown.providerTotalDefault';
if (txIsCompleted(transaction)) {
providerTotalMessageId = 'BookingBreakdown.providerTotalDelivered';
} else if (txIsDeclinedOrExpired(transaction)) {
providerTotalMessageId = 'BookingBreakdown.providerTotalDeclined';
} else if (txIsCanceled(transaction)) {
providerTotalMessageId = 'BookingBreakdown.providerTotalCanceled';
}
const totalLabel = isProvider ? (
<FormattedMessage id={providerTotalMessageId} />
) : (
<FormattedMessage id="BookingBreakdown.total" />
);
const totalPrice = isProvider
? transaction.attributes.payoutTotal
: transaction.attributes.payinTotal;
const formattedTotalPrice = formatMoney(intl, totalPrice);
return (
<div className={classes}>
<UnitPriceItem transaction={transaction} unitType={unitType} intl={intl} />
<UnitsItem transaction={transaction} booking={booking} unitType={unitType} intl={intl} />
<SubTotalItemMaybe
<LineItemUnitPrice transaction={transaction} unitType={unitType} intl={intl} />
<LineItemBookingPeriod transaction={transaction} booking={booking} unitType={unitType} />
<LineItemUnitsMaybe transaction={transaction} unitType={unitType} />
<LineItemSubTotalMaybe
transaction={transaction}
unitType={unitType}
isProvider={isProvider}
intl={intl}
/>
<CommissionItemMaybe transaction={transaction} isProvider={isProvider} intl={intl} />
<RefundItemMaybe transaction={transaction} unitType={unitType} intl={intl} />
<LineItemCommissionMaybe transaction={transaction} isProvider={isProvider} intl={intl} />
<LineItemRefundMaybe transaction={transaction} unitType={unitType} intl={intl} />
<hr className={css.totalDivider} />
<div className={css.lineItem}>
<div className={css.totalLabel}>{totalLabel}</div>
<div className={css.totalPrice}>{formattedTotalPrice}</div>
</div>
<LineItemTotalPrice transaction={transaction} isProvider={isProvider} intl={intl} />
</div>
);
};

View file

@ -0,0 +1,80 @@
import React from 'react';
import { FormattedMessage, FormattedHTMLMessage, FormattedDate } from 'react-intl';
import moment from 'moment';
import { LINE_ITEM_NIGHT, propTypes } from '../../util/types';
import { daysBetween } from '../../util/dates';
import css from './BookingBreakdown.css';
const BookingPeriod = props => {
const { isSingleDay, startDate, endDate } = props;
const dateFormatOptions = {
weekday: 'short',
month: 'short',
day: 'numeric',
};
if (isSingleDay) {
return <FormattedDate value={startDate} {...dateFormatOptions} />;
}
return (
<FormattedMessage
id="BookingBreakdown.bookingPeriod"
values={{
bookingStart: (
<span className={css.nowrap}>
<FormattedDate value={startDate} {...dateFormatOptions} />
</span>
),
bookingEnd: (
<span className={css.nowrap}>
<FormattedDate value={endDate} {...dateFormatOptions} />
</span>
),
}}
/>
);
};
const LineItemBookingPeriod = props => {
const { transaction, booking, unitType } = props;
const { start: startDate, end: endDateRaw } = booking.attributes;
const isNightly = unitType === LINE_ITEM_NIGHT;
const isDaily = unitType === LINE_ITEM_NIGHT;
const dayCount = daysBetween(startDate, endDateRaw);
const isSingleDay = !isNightly && dayCount === 1;
const endDay = isNightly ? endDateRaw : moment(endDateRaw).subtract(1, 'days');
const unitPurchase = transaction.attributes.lineItems.find(
item => item.code === unitType && !item.reversal
);
const dayCountForLineItemUnits = dayCount + 1;
const count = isNightly || isDaily ? unitPurchase.quantity.toFixed() : dayCountForLineItemUnits;
const unitCountMessage = (
<FormattedHTMLMessage
id={isNightly ? 'BookingBreakdown.nightCount' : 'BookingBreakdown.dayCount'}
values={{ count }}
/>
);
return (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<BookingPeriod isSingleDay={isSingleDay} startDate={startDate} endDate={endDay} />
</span>
<span className={css.itemValue}>{unitCountMessage}</span>
</div>
);
};
LineItemBookingPeriod.propTypes = {
transaction: propTypes.transaction.isRequired,
booking: propTypes.booking.isRequired,
};
export default LineItemBookingPeriod;

View file

@ -0,0 +1,64 @@
import React from 'react';
import { bool } from 'prop-types';
import { FormattedMessage, intlShape } from 'react-intl';
import { formatMoney } from '../../util/currency';
import { types as sdkTypes } from '../../util/sdkLoader';
import { LINE_ITEM_PROVIDER_COMMISSION, propTypes } from '../../util/types';
import css from './BookingBreakdown.css';
const { Money } = sdkTypes;
// Validate the assumption that the commission exists and the amount
// is zero or negative.
const isValidCommission = commissionLineItem => {
return (
commissionLineItem &&
commissionLineItem.lineTotal instanceof Money &&
commissionLineItem.lineTotal.amount <= 0
);
};
const LineItemCommissionMaybe = props => {
const { transaction, isProvider, intl } = props;
const providerCommissionLineItem = transaction.attributes.lineItems.find(
item => item.code === LINE_ITEM_PROVIDER_COMMISSION && !item.reversal
);
const commissionRefund = transaction.attributes.lineItems.find(
item => item.code === LINE_ITEM_PROVIDER_COMMISSION && item.reversal
);
// If commission is passed it will be shown as a fee already reduces from the total price
let commissionItem = null;
if (isProvider) {
if (!isValidCommission(providerCommissionLineItem)) {
// eslint-disable-next-line no-console
console.error('invalid commission line item:', providerCommissionLineItem);
throw new Error('Commission should be present and the value should be zero or negative');
}
const commission = providerCommissionLineItem.lineTotal;
const formattedCommission = commission ? formatMoney(intl, commission) : null;
commissionItem = commissionRefund ? null : (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.commission" />
</span>
<span className={css.itemValue}>{formattedCommission}</span>
</div>
);
}
return commissionItem;
};
LineItemCommissionMaybe.propTypes = {
transaction: propTypes.transaction.isRequired,
isProvider: bool.isRequired,
intl: intlShape.isRequired,
};
export default LineItemCommissionMaybe;

View file

@ -0,0 +1,30 @@
import React from 'react';
import { FormattedMessage, intlShape } from 'react-intl';
import { formatMoney } from '../../util/currency';
import { propTypes } from '../../util/types';
import css from './BookingBreakdown.css';
const LineItemRefundMaybe = props => {
const { transaction, unitType, intl } = props;
const refund = transaction.attributes.lineItems.find(
item => item.code === unitType && item.reversal
);
return refund ? (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.refund" />
</span>
<span className={css.itemValue}>{formatMoney(intl, refund.lineTotal)}</span>
</div>
) : null;
};
LineItemRefundMaybe.propTypes = {
transaction: propTypes.transaction.isRequired,
intl: intlShape.isRequired,
};
export default LineItemRefundMaybe;

View file

@ -0,0 +1,41 @@
import React from 'react';
import { bool } from 'prop-types';
import { FormattedMessage, intlShape } from 'react-intl';
import { formatMoney } from '../../util/currency';
import { propTypes } from '../../util/types';
import css from './BookingBreakdown.css';
const LineItemSubTotalMaybe = props => {
const { transaction, unitType, isProvider, intl } = props;
const refund = transaction.attributes.lineItems.find(
item => item.code === unitType && item.reversal
);
// Show unit purchase line total (unit price * quantity) as a subtotal.
// PLEASE NOTE that this assumes that the transaction doesn't have other
// line item types than the defined unit type (e.g. week, month, year).
const showSubTotal = isProvider || refund;
const unitPurchase = transaction.attributes.lineItems.find(
item => item.code === unitType && !item.reversal
);
const formattedSubTotal = formatMoney(intl, unitPurchase.lineTotal);
return showSubTotal ? (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.subTotal" />
</span>
<span className={css.itemValue}>{formattedSubTotal}</span>
</div>
) : null;
};
LineItemSubTotalMaybe.propTypes = {
transaction: propTypes.transaction.isRequired,
isProvider: bool.isRequired,
intl: intlShape.isRequired,
};
export default LineItemSubTotalMaybe;

View file

@ -0,0 +1,46 @@
import React from 'react';
import { bool } from 'prop-types';
import { FormattedMessage, intlShape } from 'react-intl';
import { formatMoney } from '../../util/currency';
import { txIsCanceled, txIsCompleted, txIsDeclinedOrExpired, propTypes } from '../../util/types';
import css from './BookingBreakdown.css';
const LineItemUnitPrice = props => {
const { transaction, isProvider, intl } = props;
let providerTotalMessageId = 'BookingBreakdown.providerTotalDefault';
if (txIsCompleted(transaction)) {
providerTotalMessageId = 'BookingBreakdown.providerTotalDelivered';
} else if (txIsDeclinedOrExpired(transaction)) {
providerTotalMessageId = 'BookingBreakdown.providerTotalDeclined';
} else if (txIsCanceled(transaction)) {
providerTotalMessageId = 'BookingBreakdown.providerTotalCanceled';
}
const totalLabel = isProvider ? (
<FormattedMessage id={providerTotalMessageId} />
) : (
<FormattedMessage id="BookingBreakdown.total" />
);
const totalPrice = isProvider
? transaction.attributes.payoutTotal
: transaction.attributes.payinTotal;
const formattedTotalPrice = formatMoney(intl, totalPrice);
return (
<div className={css.lineItem}>
<div className={css.totalLabel}>{totalLabel}</div>
<div className={css.totalPrice}>{formattedTotalPrice}</div>
</div>
);
};
LineItemUnitPrice.propTypes = {
transaction: propTypes.transaction.isRequired,
isProvider: bool.isRequired,
intl: intlShape.isRequired,
};
export default LineItemUnitPrice;

View file

@ -0,0 +1,37 @@
import React from 'react';
import { FormattedMessage, intlShape } from 'react-intl';
import { formatMoney } from '../../util/currency';
import { LINE_ITEM_NIGHT, LINE_ITEM_DAY, propTypes } from '../../util/types';
import css from './BookingBreakdown.css';
const LineItemUnitPrice = props => {
const { transaction, unitType, intl } = props;
const isNightly = unitType === LINE_ITEM_NIGHT;
const isDaily = unitType === LINE_ITEM_DAY;
const translationKey = isNightly
? 'BookingBreakdown.pricePerNight'
: isDaily ? 'BookingBreakdown.pricePerDay' : 'BookingBreakdown.pricePerQuantity';
const unitPurchase = transaction.attributes.lineItems.find(
item => item.code === unitType && !item.reversal
);
const formattedUnitPrice = formatMoney(intl, unitPurchase.unitPrice);
return (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id={translationKey} />
</span>
<span className={css.itemValue}>{formattedUnitPrice}</span>
</div>
);
};
LineItemUnitPrice.propTypes = {
transaction: propTypes.transaction.isRequired,
unitType: propTypes.bookingUnitType.isRequired,
intl: intlShape.isRequired,
};
export default LineItemUnitPrice;

View file

@ -0,0 +1,36 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { LINE_ITEM_UNITS, propTypes } from '../../util/types';
import css from './BookingBreakdown.css';
const LineItemUnitsMaybe = props => {
const { transaction, unitType } = props;
if (unitType !== LINE_ITEM_UNITS) {
return null;
}
const unitPurchase = transaction.attributes.lineItems.find(
item => item.code === unitType && !item.reversal
);
const quantity = unitPurchase.quantity;
return (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.quantityUnit" />
</span>
<span className={css.itemValue}>
<FormattedMessage id="BookingBreakdown.quantity" values={{ quantity }} />
</span>
</div>
);
};
LineItemUnitsMaybe.propTypes = {
transaction: propTypes.transaction.isRequired,
unitType: propTypes.bookingUnitType.isRequired,
};
export default LineItemUnitsMaybe;

View file

@ -18,7 +18,7 @@ const i18n = {
// The transaction line item code for the main unit type in bookings.
//
// Possible values: ['line-item/night', 'line-item/day']
// Possible values: ['line-item/night', 'line-item/day', 'line-item/units';]
//
// Note: if you change this, many of the generic translations will
// still show information about nights. Make sure to go through the

View file

@ -14,6 +14,7 @@ import { unitDivisor, convertMoneyToNumber, convertUnitToSubUnit } from '../../u
import {
LINE_ITEM_DAY,
LINE_ITEM_NIGHT,
LINE_ITEM_UNITS,
TRANSITION_REQUEST,
TX_TRANSITION_ACTOR_CUSTOMER,
propTypes,
@ -37,12 +38,15 @@ const estimatedTotalPrice = (unitPrice, unitCount) => {
// When we cannot speculatively initiate a transaction (i.e. logged
// out), we must estimate the booking breakdown. This function creates
// an estimated transaction object for that use case.
const estimatedTransaction = (unitType, bookingStart, bookingEnd, unitPrice) => {
const estimatedTransaction = (unitType, bookingStart, bookingEnd, unitPrice, quantity) => {
const now = new Date();
const isNightly = unitType === LINE_ITEM_NIGHT;
const isDaily = unitType === LINE_ITEM_DAY;
const unitCount = isNightly
? nightsBetween(bookingStart, bookingEnd)
: daysBetween(bookingStart, bookingEnd);
: isDaily ? daysBetween(bookingStart, bookingEnd) : quantity;
const totalPrice = estimatedTotalPrice(unitPrice, unitCount);
return {
@ -56,7 +60,7 @@ const estimatedTransaction = (unitType, bookingStart, bookingEnd, unitPrice) =>
payoutTotal: totalPrice,
lineItems: [
{
code: isNightly ? LINE_ITEM_NIGHT : LINE_ITEM_DAY,
code: unitType,
includeFor: ['customer', 'provider'],
unitPrice: unitPrice,
quantity: new Decimal(unitCount),
@ -83,13 +87,15 @@ const estimatedTransaction = (unitType, bookingStart, bookingEnd, unitPrice) =>
};
};
const estimatedBreakdown = (unitType, bookingStart, bookingEnd, unitPrice) => {
const canEstimatePrice = bookingStart && bookingEnd && unitPrice;
const estimatedBreakdown = (unitType, bookingStart, bookingEnd, unitPrice, quantity) => {
const isUnits = unitType === LINE_ITEM_UNITS;
const quantityIfUsingUnits = !isUnits || Number.isInteger(quantity);
const canEstimatePrice = bookingStart && bookingEnd && unitPrice && quantityIfUsingUnits;
if (!canEstimatePrice) {
return null;
}
const tx = estimatedTransaction(unitType, bookingStart, bookingEnd, unitPrice);
const tx = estimatedTransaction(unitType, bookingStart, bookingEnd, unitPrice, quantity);
return (
<BookingBreakdown

View file

@ -21,6 +21,7 @@ export const SPECULATE_TRANSACTION_ERROR = 'app/ListingPage/SPECULATE_TRANSACTIO
const initialState = {
listing: null,
bookingData: null,
bookingDates: null,
speculateTransactionInProgress: false,
speculateTransactionError: null,
@ -154,18 +155,12 @@ export const initiateOrder = (orderParams, initialMessage) => (dispatch, getStat
* pricing info for the booking breakdown to get a proper estimate for
* the price with the chosen information.
*/
export const speculateTransaction = (listingId, bookingStart, bookingEnd) => (
dispatch,
getState,
sdk
) => {
export const speculateTransaction = params => (dispatch, getState, sdk) => {
dispatch(speculateTransactionRequest());
const bodyParams = {
transition: TRANSITION_REQUEST,
params: {
listingId,
bookingStart,
bookingEnd,
...params,
cardToken: 'CheckoutPage_speculative_card_token',
},
};
@ -184,10 +179,11 @@ export const speculateTransaction = (listingId, bookingStart, bookingEnd) => (
dispatch(speculateTransactionSuccess(tx));
})
.catch(e => {
const { listingId, bookingStart, bookingEnd } = params;
log.error(e, 'speculate-transaction-failed', {
listingId: listingId.uuid,
bookingStart: bookingStart,
bookingEnd: bookingEnd,
bookingStart,
bookingEnd,
});
return dispatch(speculateTransactionError(storableError(e)));
});

View file

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bool, func, instanceOf, object, shape, string } from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
@ -70,28 +70,33 @@ export class CheckoutPageComponent extends Component {
* based on this initial data.
*/
loadInitialData() {
const { bookingDates, listing } = this.props;
const hasDataInProps = !!(bookingDates && listing);
const pageData = hasDataInProps ? { bookingDates, listing } : storedData(STORAGE_KEY);
const { bookingData, bookingDates, listing, fetchSpeculatedTransaction } = this.props;
const hasDataInProps = !!(bookingData && bookingDates && listing);
const pageData = hasDataInProps
? { bookingData, bookingDates, listing }
: storedData(STORAGE_KEY);
if (hasDataInProps) {
storeData(bookingDates, listing, STORAGE_KEY);
storeData(bookingData, bookingDates, listing, STORAGE_KEY);
}
const hasData =
pageData &&
pageData.listing &&
pageData.listing.id &&
pageData.bookingData &&
pageData.bookingDates &&
pageData.bookingDates.bookingStart &&
pageData.bookingDates.bookingEnd;
if (hasData) {
this.props.fetchSpeculatedTransaction(
pageData.listing.id,
pageData.bookingDates.bookingStart,
pageData.bookingDates.bookingEnd
);
const listingId = pageData.listing.id;
const { bookingStart, bookingEnd } = pageData.bookingDates;
// Fetch speculated transaction for showing price in booking breakdown
// NOTE: if unit type is line-item/units, quantity needs to be added.
// The way to pass it to checkout page is through pageData.bookingData
fetchSpeculatedTransaction({ listingId, bookingStart, bookingEnd });
}
this.setState({ pageData: pageData || {}, dataLoaded: true });
@ -106,6 +111,10 @@ export class CheckoutPageComponent extends Component {
const cardToken = values.token;
const initialMessage = values.message;
const { history, sendOrderRequest, speculatedTransaction, dispatch } = this.props;
// Create order aka transaction
// NOTE: if unit type is line-item/units, quantity needs to be added.
// The way to pass it to checkout page is through pageData.bookingData
const requestParams = {
listingId: this.state.pageData.listing.id,
cardToken,
@ -387,17 +396,17 @@ export class CheckoutPageComponent extends Component {
CheckoutPageComponent.defaultProps = {
initiateOrderError: null,
listing: null,
bookingData: {},
bookingDates: null,
speculateTransactionError: null,
speculatedTransaction: null,
currentUser: null,
};
const { func, instanceOf, shape, string, bool } = PropTypes;
CheckoutPageComponent.propTypes = {
scrollingDisabled: bool.isRequired,
listing: propTypes.listing,
bookingData: object,
bookingDates: shape({
bookingStart: instanceOf(Date).isRequired,
bookingEnd: instanceOf(Date).isRequired,
@ -429,6 +438,7 @@ CheckoutPageComponent.propTypes = {
const mapStateToProps = state => {
const {
listing,
bookingData,
bookingDates,
speculateTransactionInProgress,
speculateTransactionError,
@ -439,6 +449,7 @@ const mapStateToProps = state => {
return {
scrollingDisabled: isScrollingDisabled(state),
currentUser,
bookingData,
bookingDates,
speculateTransactionInProgress,
speculateTransactionError,

View file

@ -54,6 +54,7 @@ describe('CheckoutPage', () => {
const initialValues = {
initiateOrderError: null,
listing: null,
bookingData: null,
bookingDates: null,
speculateTransactionError: null,
speculateTransactionInProgress: false,

View file

@ -47,12 +47,13 @@ export const isValidListing = listing => {
};
// Stores given bookingDates and listing to sessionStorage
export const storeData = (bookingDates, listing, storageKey) => {
if (window && window.sessionStorage && listing && bookingDates) {
export const storeData = (bookingData, bookingDates, listing, storageKey) => {
if (window && window.sessionStorage && listing && bookingDates && bookingData) {
// TODO: How should we deal with Dates when data is serialized?
// Hard coded serializable date objects atm.
/* eslint-disable no-underscore-dangle */
const data = {
bookingData,
bookingDates: {
bookingStart: { date: bookingDates.bookingStart, _serializedType: 'SerializableDate' },
bookingEnd: { date: bookingDates.bookingEnd, _serializedType: 'SerializableDate' },
@ -82,7 +83,7 @@ export const storedData = storageKey => {
return sdkTypes.reviver(k, v);
};
const { bookingDates, listing, storedAt } = checkoutPageData
const { bookingData, bookingDates, listing, storedAt } = checkoutPageData
? JSON.parse(checkoutPageData, reviver)
: {};
@ -92,7 +93,7 @@ export const storedData = storageKey => {
: false;
if (isFreshlySaved && isValidBookingDates(bookingDates) && isValidListing(listing)) {
return { bookingDates, listing };
return { bookingData, bookingDates, listing };
}
}
return {};

View file

@ -172,10 +172,11 @@ export class ListingPageComponent extends Component {
const listingId = new UUID(params.id);
const listing = getListing(listingId);
const { bookingDates } = values;
const { bookingDates, ...bookingData } = values;
const initialValues = {
listing,
bookingData,
bookingDates: {
bookingStart: bookingDates.startDate,
bookingEnd: bookingDates.endDate,

View file

@ -43,10 +43,13 @@
"BookingBreakdown.nightCount": "{count, number} {count, plural, one {night} other {nights}}",
"BookingBreakdown.pricePerDay": "Price per day",
"BookingBreakdown.pricePerNight": "Price per night",
"BookingBreakdown.pricePerQuantity": "Price per person",
"BookingBreakdown.providerTotalCanceled": "Total price",
"BookingBreakdown.providerTotalDeclined": "You would have made",
"BookingBreakdown.providerTotalDefault": "You'll make",
"BookingBreakdown.providerTotalDelivered": "You made",
"BookingBreakdown.quantityUnit": "Number of persons",
"BookingBreakdown.quantity": "{quantity, number} {quantity, plural, one {person} other {persons}}",
"BookingBreakdown.refund": "Refund",
"BookingBreakdown.subTotal": "Subtotal",
"BookingBreakdown.total": "Total price",

View file

@ -320,11 +320,12 @@ propTypes.review = shape({
export const LINE_ITEM_NIGHT = 'line-item/night';
export const LINE_ITEM_DAY = 'line-item/day';
export const LINE_ITEM_UNITS = 'line-item/units';
export const LINE_ITEM_PROVIDER_COMMISSION = 'line-item/provider-commission';
const LINE_ITEMS = [LINE_ITEM_NIGHT, LINE_ITEM_DAY, LINE_ITEM_PROVIDER_COMMISSION];
const LINE_ITEMS = [LINE_ITEM_NIGHT, LINE_ITEM_DAY, LINE_ITEM_UNITS, LINE_ITEM_PROVIDER_COMMISSION];
propTypes.bookingUnitType = oneOf([LINE_ITEM_NIGHT, LINE_ITEM_DAY]);
propTypes.bookingUnitType = oneOf([LINE_ITEM_NIGHT, LINE_ITEM_DAY, LINE_ITEM_UNITS]);
// Denormalised transaction object
propTypes.transaction = shape({