mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 03:43:28 +10:00
Merge pull request #684 from sharetribe/refactor-booking-flow-for-units
Refactor booking flow for units
This commit is contained in:
commit
a2a6e62036
17 changed files with 409 additions and 267 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
80
src/components/BookingBreakdown/LineItemBookingPeriod.js
Normal file
80
src/components/BookingBreakdown/LineItemBookingPeriod.js
Normal 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;
|
||||
64
src/components/BookingBreakdown/LineItemCommissionMaybe.js
Normal file
64
src/components/BookingBreakdown/LineItemCommissionMaybe.js
Normal 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;
|
||||
30
src/components/BookingBreakdown/LineItemRefundMaybe.js
Normal file
30
src/components/BookingBreakdown/LineItemRefundMaybe.js
Normal 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;
|
||||
41
src/components/BookingBreakdown/LineItemSubTotalMaybe.js
Normal file
41
src/components/BookingBreakdown/LineItemSubTotalMaybe.js
Normal 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;
|
||||
46
src/components/BookingBreakdown/LineItemTotalPrice.js
Normal file
46
src/components/BookingBreakdown/LineItemTotalPrice.js
Normal 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;
|
||||
37
src/components/BookingBreakdown/LineItemUnitPrice.js
Normal file
37
src/components/BookingBreakdown/LineItemUnitPrice.js
Normal 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;
|
||||
36
src/components/BookingBreakdown/LineItemUnitsMaybe.js
Normal file
36
src/components/BookingBreakdown/LineItemUnitsMaybe.js
Normal 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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ describe('CheckoutPage', () => {
|
|||
const initialValues = {
|
||||
initiateOrderError: null,
|
||||
listing: null,
|
||||
bookingData: null,
|
||||
bookingDates: null,
|
||||
speculateTransactionError: null,
|
||||
speculateTransactionInProgress: false,
|
||||
|
|
|
|||
|
|
@ -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 {};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue