Showing commission (and calculating subtotal and total)

This commit is contained in:
Vesa Luusua 2017-04-27 16:42:18 +03:00
parent a59c01486a
commit d70b1f83cf
11 changed files with 286 additions and 32 deletions

View file

@ -1,7 +1,7 @@
import { types } from '../../util/sdkLoader';
import BookingInfo from './BookingInfo';
export const Empty = {
export const BeforeTX = {
component: BookingInfo,
props: {
unitPrice: new types.Money(10, 'USD'),
@ -9,3 +9,36 @@ export const Empty = {
bookingEnd: new Date(Date.UTC(2017, 3, 16)),
},
};
export const TXCustomerSide = {
component: BookingInfo,
props: {
unitPrice: new types.Money(10, 'USD'),
bookingStart: new Date(Date.UTC(2017, 3, 14)),
bookingEnd: new Date(Date.UTC(2017, 3, 16)),
totalPrice: new types.Money(20, 'USD'),
},
};
export const TXProviderSide = {
component: BookingInfo,
props: {
unitPrice: new types.Money(10, 'USD'),
bookingStart: new Date(Date.UTC(2017, 3, 14)),
bookingEnd: new Date(Date.UTC(2017, 3, 16)),
subtotalPrice: new types.Money(20, 'USD'),
commission: new types.Money(2, 'USD'),
},
};
export const TXNoCalculation = {
component: BookingInfo,
props: {
unitPrice: new types.Money(10, 'USD'),
bookingStart: new Date(Date.UTC(2017, 3, 14)),
bookingEnd: new Date(Date.UTC(2017, 3, 16)),
subtotalPrice: new types.Money(20, 'USD'),
commission: new types.Money(2, 'USD'),
totalPrice: new types.Money(18, 'USD'),
},
};

View file

@ -13,39 +13,80 @@ import { types } from '../../util/sdkLoader';
import css from './BookingInfo.css';
const BookingInfoComponent = props => {
const { bookingStart, bookingEnd, className, intl, totalPrice, unitPrice } = props;
const { bookingStart, bookingEnd, className, commission, intl, subtotalPrice, totalPrice, unitPrice } = props;
const hasSelectedDays = bookingStart && bookingEnd;
const bookingPeriod = hasSelectedDays
? <FormattedMessage
id="BookingInfo.bookingPeriod"
values={{
bookingStart: intl.formatDate(bookingStart),
bookingEnd: intl.formatDate(bookingEnd),
}}
/>
: '';
const nightCount = hasSelectedDays ? moment(bookingEnd).diff(moment(bookingStart), 'days') : null;
const nightCountMessage = nightCount
? <FormattedMessage id="BookingInfo.nightCount" values={{ count: nightCount }} />
: null;
const hasSelectedNights = bookingStart && bookingEnd;
// If there's not enough info, we print empty container
if (!hasSelectedNights) {
return <div className={classNames(css.container, className)} />;
}
const bookingPeriod = <FormattedMessage
id="BookingInfo.bookingPeriod"
values={{
bookingStart: intl.formatDate(bookingStart),
bookingEnd: intl.formatDate(bookingEnd),
}}
/>;
// diff gives night count between dates
const nightCount = moment(bookingEnd).diff(moment(bookingStart), 'days');
const nightCountMessage = <FormattedMessage id="BookingInfo.nightCount" values={{ count: nightCount }} />;
const currencyConfig = config.currencyConfig;
const subUnitDivisor = currencyConfig.subUnitDivisor;
const unitPriceAsNumber = convertMoneyToNumber(unitPrice, subUnitDivisor);
const formattedUnitPrice = intl.formatNumber(unitPriceAsNumber, currencyConfig);
const calculatedTotalPriceAsNumber = !totalPrice && hasSelectedDays
? new Decimal(unitPriceAsNumber).times(nightCount).toNumber()
// Subtotal price can be given (when it comes from API)
// or calculated: unit price * booked nights
const subtotalPriceAsNumber = subtotalPrice
? convertMoneyToNumber(subtotalPrice, subUnitDivisor)
: new Decimal(unitPriceAsNumber).times(nightCount).toNumber();
const formattedSubtotal = commission
? intl.formatNumber(subtotalPriceAsNumber, currencyConfig)
: null;
// Total price can be given (when it comes from API) or calculated based on unit price and nights
// If commission is passed it will be reduced from sub total.
const commissionAsNumber = commission ? convertMoneyToNumber(commission, subUnitDivisor) : 0;
const formattedCommission = commission
? intl.formatNumber(new Decimal(commissionAsNumber).negated().toNumber(), currencyConfig)
: null;
// Total price can be given (when it comes from API)
// or calculated: sub total - commission
const totalPriceAsNumber = totalPrice
? convertMoneyToNumber(totalPrice, subUnitDivisor)
: calculatedTotalPriceAsNumber;
: new Decimal(subtotalPriceAsNumber).minus(commissionAsNumber).toNumber();
const formattedTotalPrice = totalPriceAsNumber
? intl.formatNumber(totalPriceAsNumber, currencyConfig)
: null;
// Sub total is shown if commission is given
const subtotalInfo = commission
? <div className={css.row}>
<div className={css.subtotalLabel}>
<FormattedMessage id="BookingInfo.subtotal" />
</div>
<div className={css.subtotal}>
{formattedSubtotal}
</div>
</div>
: null;
const commisionInfo = commission
? <div className={css.row}>
<div className={css.commissionLabel}>
<FormattedMessage id="BookingInfo.commission" />
</div>
<div className={css.commission}>
{formattedCommission}
</div>
</div>
: null;
return (
<div className={classNames(css.container, className)}>
<div className={css.row}>
@ -66,6 +107,8 @@ const BookingInfoComponent = props => {
{nightCountMessage}
</div>
</div>
{subtotalInfo}
{commisionInfo}
<hr className={css.totalDivider} />
<div className={css.row}>
<div className={css.totalLabel}>
@ -83,7 +126,9 @@ BookingInfoComponent.defaultProps = {
bookingStart: null,
bookingEnd: null,
className: '',
subtotalPrice: null,
totalPrice: null,
commission: null,
};
const { instanceOf, string } = PropTypes;
@ -92,7 +137,9 @@ BookingInfoComponent.propTypes = {
bookingStart: instanceOf(Date),
bookingEnd: instanceOf(Date),
className: string,
commission: instanceOf(types.Money),
intl: intlShape.isRequired,
subtotalPrice: instanceOf(types.Money),
totalPrice: instanceOf(types.Money),
unitPrice: instanceOf(types.Money).isRequired,
};

View file

@ -5,7 +5,7 @@ import { types } from '../../util/sdkLoader';
import BookingInfo from './BookingInfo';
describe('BookingInfo', () => {
it('matches snapshot', () => {
it('pretransaction data matches snapshot', () => {
const tree = renderDeep(
<BookingInfo
unitPrice={new types.Money(1000, 'USD')}
@ -16,4 +16,31 @@ describe('BookingInfo', () => {
);
expect(tree).toMatchSnapshot();
});
it('customer transaction data matches snapshot', () => {
const tree = renderDeep(
<BookingInfo
unitPrice={new types.Money(1000, 'USD')}
bookingStart={new Date(Date.UTC(2017, 3, 14))}
bookingEnd={new Date(Date.UTC(2017, 3, 16))}
totalPrice={new types.Money(2000, 'USD')}
intl={fakeIntl}
/>
);
expect(tree).toMatchSnapshot();
});
it('provider transaction data matches snapshot', () => {
const tree = renderDeep(
<BookingInfo
unitPrice={new types.Money(1000, 'USD')}
bookingStart={new Date(Date.UTC(2017, 3, 14))}
bookingEnd={new Date(Date.UTC(2017, 3, 16))}
subtotalPrice={new types.Money(2000, 'USD')}
commission={new types.Money(200, 'USD')}
intl={fakeIntl}
/>
);
expect(tree).toMatchSnapshot();
});
});

View file

@ -1,4 +1,4 @@
exports[`BookingInfo matches snapshot 1`] = `
exports[`BookingInfo customer transaction data matches snapshot 1`] = `
<div
className="">
<div
@ -50,3 +50,135 @@ exports[`BookingInfo matches snapshot 1`] = `
</div>
</div>
`;
exports[`BookingInfo pretransaction data matches snapshot 1`] = `
<div
className="">
<div
className={undefined}>
<div
className={undefined}>
<span>
Price per night:
</span>
</div>
<div
className={undefined}>
$10.00
</div>
</div>
<div
className={undefined}>
<div
className={undefined}>
<span>
Booking period:
</span>
<br />
<span>
4/14/2017 - 4/16/2017
</span>
</div>
<div
className={undefined}>
<span>
2 nights
</span>
</div>
</div>
<hr
className={undefined} />
<div
className={undefined}>
<div
className={undefined}>
<span>
Total:
</span>
</div>
<div
className={undefined}>
$20.00
</div>
</div>
</div>
`;
exports[`BookingInfo provider transaction data matches snapshot 1`] = `
<div
className="">
<div
className={undefined}>
<div
className={undefined}>
<span>
Price per night:
</span>
</div>
<div
className={undefined}>
$10.00
</div>
</div>
<div
className={undefined}>
<div
className={undefined}>
<span>
Booking period:
</span>
<br />
<span>
4/14/2017 - 4/16/2017
</span>
</div>
<div
className={undefined}>
<span>
2 nights
</span>
</div>
</div>
<div
className={undefined}>
<div
className={undefined}>
<span>
Subtotal:
</span>
</div>
<div
className={undefined}>
$20.00
</div>
</div>
<div
className={undefined}>
<div
className={undefined}>
<span>
Charged commission:
</span>
</div>
<div
className={undefined}>
-$2.00
</div>
</div>
<hr
className={undefined} />
<div
className={undefined}>
<div
className={undefined}>
<span>
Total:
</span>
</div>
<div
className={undefined}>
$18.00
</div>
</div>
</div>
`;

View file

@ -8,7 +8,7 @@ import { Avatar, BookingInfo, NamedLink } from '../../components';
import css from './SaleDetailsPanel.css';
const SaleDetailsPanel = props => {
const { className, totalPrice, saleState, booking, listing, customer } = props;
const { className, subtotalPrice, saleState, booking, listing, customer, commission } = props;
const { firstName, lastName } = customer.attributes.profile;
const customerName = firstName ? `${firstName} ${lastName}` : '';
@ -42,7 +42,8 @@ const SaleDetailsPanel = props => {
bookingStart={booking.attributes.start}
bookingEnd={booking.attributes.end}
unitPrice={unitPrice}
totalPrice={totalPrice}
commission={commission}
subtotalPrice={subtotalPrice}
/>
: <p className={css.error}>{'priceRequiredMessage'}</p>;
@ -66,7 +67,8 @@ const { instanceOf, string } = PropTypes;
SaleDetailsPanel.propTypes = {
className: string,
totalPrice: instanceOf(types.Money).isRequired,
subtotalPrice: instanceOf(types.Money).isRequired,
commission: instanceOf(types.Money).isRequired,
saleState: string.isRequired,
booking: propTypes.booking.isRequired,
listing: propTypes.listing.isRequired,

View file

@ -8,8 +8,8 @@ describe('SaleDetailsPanel', () => {
it('matches snapshot', () => {
const { Money } = types;
const props = {
commission: null,
totalPrice: new Money(16500, 'USD'),
commission: new Money(1650, 'USD'),
subtotalPrice: new Money(16500, 'USD'),
saleState: 'state/preauthorized',
booking: createBooking(
'booking1',

View file

@ -39,7 +39,13 @@ exports[`SaleDetailsPanel matches snapshot 1`] = `
<BookingInfo
bookingEnd={2017-06-13T00:00:00.000Z}
bookingStart={2017-06-10T00:00:00.000Z}
totalPrice={
commission={
Money {
"amount": 1650,
"currency": "USD",
}
}
subtotalPrice={
Money {
"amount": 16500,
"currency": "USD",

View file

@ -19,7 +19,8 @@ export const SalePageComponent = props => {
const title = currentListing.attributes.title;
const detailsProps = {
totalPrice: currentTransaction.attributes.total,
subtotalPrice: currentTransaction.attributes.total,
commission: currentTransaction.attributes.commission,
saleState: currentTransaction.attributes.state,
listing: currentListing,
booking: ensureBooking(currentTransaction.booking),

View file

@ -15,13 +15,18 @@ exports[`SalePage matches snapshot 1`] = `
}
}
className="undefined"
commission={
Money {
"amount": 100,
"currency": "USD",
}
}
customer={
Object {
"attributes": Object {
"profile": Object {
"firstName": "customer1 first name",
"lastName": "customer1 last name",
"slug": "customer1-slug",
},
},
"id": UUID {
@ -54,7 +59,7 @@ exports[`SalePage matches snapshot 1`] = `
}
}
saleState="state/preauthorized"
totalPrice={
subtotalPrice={
Money {
"amount": 1000,
"currency": "USD",

View file

@ -15,6 +15,7 @@
"BookingInfo.commission": "Charged commission:",
"BookingInfo.nightCount": "{count, number} {count, plural, one {night} other {nights}}",
"BookingInfo.pricePerDay":"Price per night:",
"BookingInfo.subtotal": "Subtotal:",
"BookingInfo.total": "Total:",
"CheckoutPage.initiateOrderError": "Payment request failed. Please try again.",
"CheckoutPage.paymentInfo": "You'll only be charged if your request is accepted by the provider.",

View file

@ -83,7 +83,7 @@ export const createTransaction = options => {
id: new UUID(id),
type: 'transaction',
attributes: {
commission: null,
commission: new Money(100, 'USD'),
createdAt: new Date(Date.UTC(2017, 4, 1)),
lastTransitionedAt,
state,