Merge pull request #257 from sharetribe/payin-payout

Use line items to show the price breakdown
This commit is contained in:
Vesa Luusua 2017-07-02 10:44:09 +03:00 committed by GitHub
commit 3cf654849e
23 changed files with 542 additions and 170 deletions

View file

@ -33,7 +33,7 @@
"redux-thunk": "^2.2.0",
"sanitize.css": "^5.0.0",
"sharetribe-scripts": "0.9.2",
"sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#a3d2bfcc64e0b91c6626b959c8395fbda39fd0d8",
"sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#a086f37c765cf5b6034bc5f4788788237485c863",
"source-map-support": "^0.4.14",
"url": "^0.11.0"
},

View file

@ -24,6 +24,7 @@ const enforceSsl = require('express-enforces-ssl');
const path = require('path');
const qs = require('qs');
const sharetribeSdk = require('sharetribe-sdk');
const Decimal = require('decimal.js');
const auth = require('./auth');
const renderer = require('./renderer');
const dataLoader = require('./dataLoader');
@ -89,6 +90,14 @@ app.get('*', (req, res) => {
req,
res,
}),
typeHandlers: [
{
type: sharetribeSdk.types.BigDecimal,
customType: Decimal,
writer: v => new sharetribeSdk.types.BigDecimal(v.toString()),
reader: v => new Decimal(v.value),
},
],
});
dataLoader

View file

@ -1,3 +1,4 @@
import Decimal from 'decimal.js';
import { types } from '../../util/sdkLoader';
import BookingBreakdown from './BookingBreakdown';
@ -6,8 +7,16 @@ export const Checkout = {
props: {
bookingStart: new Date(Date.UTC(2017, 3, 14)),
bookingEnd: new Date(Date.UTC(2017, 3, 16)),
unitPrice: new types.Money(4500, 'USD'),
totalPrice: new types.Money(9000, 'USD'),
userRole: 'customer',
lineItems: [
{
code: 'line-item/night',
quantity: new Decimal(2),
unitPrice: new types.Money(4500, 'USD'),
lineTotal: new types.Money(9000, 'USD'),
},
],
payinTotal: new types.Money(9000, 'USD'),
},
};
@ -16,8 +25,16 @@ export const CustomerOrder = {
props: {
bookingStart: new Date(Date.UTC(2017, 3, 14)),
bookingEnd: new Date(Date.UTC(2017, 3, 16)),
unitPrice: new types.Money(4500, 'USD'),
totalPrice: new types.Money(9000, 'USD'),
userRole: 'customer',
lineItems: [
{
code: 'line-item/night',
quantity: new Decimal(2),
unitPrice: new types.Money(4500, 'USD'),
lineTotal: new types.Money(9000, 'USD'),
},
],
payinTotal: new types.Money(9000, 'USD'),
},
};
@ -26,9 +43,21 @@ export const ProviderSale = {
props: {
bookingStart: new Date(Date.UTC(2017, 3, 14)),
bookingEnd: new Date(Date.UTC(2017, 3, 16)),
unitPrice: new types.Money(4500, 'USD'),
commission: new types.Money(2000, 'USD'),
totalPrice: new types.Money(7000, 'USD'),
userRole: 'provider',
lineItems: [
{
code: 'line-item/night',
quantity: new Decimal(2),
unitPrice: new types.Money(4500, 'USD'),
lineTotal: new types.Money(9000, 'USD'),
},
{
code: 'line-item/provider-commission',
unitPrice: new types.Money(-2000, 'USD'),
lineTotal: new types.Money(-2000, 'USD'),
},
],
payoutTotal: new types.Money(7000, 'USD'),
},
};
@ -37,9 +66,21 @@ export const ProviderSaleZeroCommission = {
props: {
bookingStart: new Date(Date.UTC(2017, 3, 14)),
bookingEnd: new Date(Date.UTC(2017, 3, 16)),
unitPrice: new types.Money(4500, 'USD'),
commission: new types.Money(0, 'USD'),
totalPrice: new types.Money(7000, 'USD'),
userRole: 'provider',
lineItems: [
{
code: 'line-item/night',
quantity: new Decimal(2),
unitPrice: new types.Money(4500, 'USD'),
lineTotal: new types.Money(9000, 'USD'),
},
{
code: 'line-item/provider-commission',
unitPrice: new types.Money(0, 'USD'),
lineTotal: new types.Money(0, 'USD'),
},
],
payoutTotal: new types.Money(9000, 'USD'),
},
};
@ -48,8 +89,20 @@ export const ProviderSaleSingleNight = {
props: {
bookingStart: new Date(Date.UTC(2017, 3, 14)),
bookingEnd: new Date(Date.UTC(2017, 3, 15)),
unitPrice: new types.Money(4500, 'USD'),
commission: new types.Money(2000, 'USD'),
totalPrice: new types.Money(2500, 'USD'),
userRole: 'provider',
lineItems: [
{
code: 'line-item/night',
quantity: new Decimal(1),
unitPrice: new types.Money(4500, 'USD'),
lineTotal: new types.Money(4500, 'USD'),
},
{
code: 'line-item/provider-commission',
unitPrice: new types.Money(-2000, 'USD'),
lineTotal: new types.Money(-2000, 'USD'),
},
],
payoutTotal: new types.Money(2500, 'USD'),
},
};

View file

@ -3,13 +3,12 @@
* I.e. dates and other details related to payment decision in receipt format.
*/
import React, { PropTypes } from 'react';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import { FormattedMessage, FormattedHTMLMessage, intlShape, injectIntl } from 'react-intl';
import Decimal from 'decimal.js';
import classNames from 'classnames';
import config from '../../config';
import { convertMoneyToNumber } from '../../util/currency';
import * as propTypes from '../../util/propTypes';
import { nightsBetween } from '../../util/dates';
import css from './BookingBreakdown.css';
@ -19,14 +18,23 @@ export const BookingBreakdownComponent = props => {
className,
bookingStart,
bookingEnd,
unitPrice,
commission,
totalPrice,
payinTotal,
payoutTotal,
lineItems,
userRole,
intl,
} = props;
const classes = classNames(rootClassName || css.root, className);
if (userRole === 'customer' && !payinTotal) {
throw new Error('payinTotal is required for customer breakdown');
}
if (userRole === 'provider' && !payoutTotal) {
throw new Error('payoutTotal is required for provider breakdown');
}
const dateFormatOptions = {
weekday: 'short',
month: 'long',
@ -50,33 +58,44 @@ export const BookingBreakdownComponent = props => {
/>
);
const nightCount = nightsBetween(bookingStart, bookingEnd);
const nightPurchase = lineItems.find(item => item.code === 'line-item/night');
const providerCommission = lineItems.find(item => item.code === 'line-item/provider-commission');
const nightCount = nightPurchase.quantity.toFixed();
const nightCountMessage = (
<FormattedMessage id="BookingBreakdown.nightCount" values={{ count: nightCount }} />
<FormattedHTMLMessage id="BookingBreakdown.nightCount" values={{ count: nightCount }} />
);
const currencyConfig = config.currencyConfig;
const subUnitDivisor = currencyConfig.subUnitDivisor;
const unitPriceAsNumber = convertMoneyToNumber(unitPrice, subUnitDivisor);
const unitPriceAsNumber = convertMoneyToNumber(nightPurchase.unitPrice, subUnitDivisor);
const formattedUnitPrice = intl.formatNumber(unitPriceAsNumber, currencyConfig);
// If commission is passed it will be shown as a fee already reduces from the total price
const commissionAsNumber = commission ? convertMoneyToNumber(commission, subUnitDivisor) : 0;
const formattedCommission = commission
? intl.formatNumber(new Decimal(commissionAsNumber).negated().toNumber(), currencyConfig)
: null;
let commissionInfo = null;
const commissionInfo = (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.commission" />
</span>
<span className={css.itemValue}>{formattedCommission}</span>
</div>
if (userRole === 'provider') {
const commission = providerCommission.lineTotal;
const commissionAsNumber = commission ? convertMoneyToNumber(commission, subUnitDivisor) : 0;
const formattedCommission = commission
? intl.formatNumber(new Decimal(commissionAsNumber).negated().toNumber(), currencyConfig)
: null;
commissionInfo = (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.commission" />
</span>
<span className={css.itemValue}>{formattedCommission}</span>
</div>
);
}
const totalPriceAsNumber = convertMoneyToNumber(
userRole === 'customer' ? payinTotal : payoutTotal,
subUnitDivisor
);
const totalPriceAsNumber = convertMoneyToNumber(totalPrice, subUnitDivisor);
const formattedTotalPrice = totalPriceAsNumber
? intl.formatNumber(totalPriceAsNumber, currencyConfig)
: null;
@ -95,7 +114,7 @@ export const BookingBreakdownComponent = props => {
</span>
<span className={css.itemValue}>{nightCountMessage}</span>
</div>
{commission ? commissionInfo : null}
{commissionInfo}
<hr className={css.totalDivider} />
<div className={css.lineItem}>
<div className={css.totalLabel}>
@ -112,10 +131,18 @@ export const BookingBreakdownComponent = props => {
BookingBreakdownComponent.defaultProps = {
rootClassName: null,
className: null,
commission: null,
payinTotal: null,
payoutTotal: null,
};
const { string, instanceOf } = PropTypes;
const { string, instanceOf, arrayOf, oneOf, shape } = PropTypes;
const lineItem = shape({
code: string.isRequired,
quantity: instanceOf(Decimal),
unitPrice: propTypes.money,
lineTotal: propTypes.money,
});
BookingBreakdownComponent.propTypes = {
rootClassName: string,
@ -123,10 +150,10 @@ BookingBreakdownComponent.propTypes = {
bookingStart: instanceOf(Date).isRequired,
bookingEnd: instanceOf(Date).isRequired,
unitPrice: propTypes.money.isRequired,
totalPrice: propTypes.money.isRequired,
commission: propTypes.money,
lineItems: arrayOf(lineItem).isRequired,
userRole: oneOf(['customer', 'provider']).isRequired,
payinTotal: propTypes.money, // required if userRole === customer
payoutTotal: propTypes.money, // required if userRole === provider
// from injectIntl
intl: intlShape.isRequired,

View file

@ -1,4 +1,5 @@
import React from 'react';
import Decimal from 'decimal.js';
import { fakeIntl } from '../../util/test-data';
import { renderDeep } from '../../util/test-helpers';
import { types } from '../../util/sdkLoader';
@ -10,8 +11,16 @@ describe('BookingBreakdown', () => {
<BookingBreakdownComponent
bookingStart={new Date(Date.UTC(2017, 3, 14))}
bookingEnd={new Date(Date.UTC(2017, 3, 16))}
unitPrice={new types.Money(1000, 'USD')}
totalPrice={new types.Money(2000, 'USD')}
payinTotal={new types.Money(2000, 'USD')}
userRole="customer"
lineItems={[
{
code: 'line-item/night',
quantity: new Decimal(2),
lineTotal: new types.Money(2000, 'USD'),
unitPrice: new types.Money(1000, 'USD'),
},
]}
intl={fakeIntl}
/>
);
@ -23,8 +32,16 @@ describe('BookingBreakdown', () => {
<BookingBreakdownComponent
bookingStart={new Date(Date.UTC(2017, 3, 14))}
bookingEnd={new Date(Date.UTC(2017, 3, 16))}
unitPrice={new types.Money(1000, 'USD')}
totalPrice={new types.Money(2000, 'USD')}
userRole="customer"
payinTotal={new types.Money(2000, 'USD')}
lineItems={[
{
code: 'line-item/night',
quantity: new Decimal(2),
lineTotal: new types.Money(2000, 'USD'),
unitPrice: new types.Money(1000, 'USD'),
},
]}
intl={fakeIntl}
/>
);
@ -36,9 +53,22 @@ describe('BookingBreakdown', () => {
<BookingBreakdownComponent
bookingStart={new Date(Date.UTC(2017, 3, 14))}
bookingEnd={new Date(Date.UTC(2017, 3, 16))}
unitPrice={new types.Money(1000, 'USD')}
totalPrice={new types.Money(1800, 'USD')}
commission={new types.Money(200, 'USD')}
payoutTotal={new types.Money(1800, 'USD')}
userRole="provider"
lineItems={[
{
code: 'line-item/night',
quantity: new Decimal(2),
lineTotal: new types.Money(2000, 'USD'),
unitPrice: new types.Money(1000, 'USD'),
},
{
code: 'line-item/provider-commission',
lineTotal: new types.Money(-200, 'USD'),
unitPrice: new types.Money(-200, 'USD'),
},
]}
intl={fakeIntl}
/>
);

View file

@ -32,9 +32,12 @@ exports[`BookingBreakdown customer transaction data matches snapshot 1`] = `
</span>
<span
className={undefined}>
<span>
* 2 nights
</span>
<span
dangerouslySetInnerHTML={
Object {
"__html": "&times; 2 nights",
}
} />
</span>
</div>
<hr
@ -89,9 +92,12 @@ exports[`BookingBreakdown pretransaction data matches snapshot 1`] = `
</span>
<span
className={undefined}>
<span>
* 2 nights
</span>
<span
dangerouslySetInnerHTML={
Object {
"__html": "&times; 2 nights",
}
} />
</span>
</div>
<hr
@ -146,9 +152,12 @@ exports[`BookingBreakdown provider transaction data matches snapshot 1`] = `
</span>
<span
className={undefined}>
<span>
* 2 nights
</span>
<span
dangerouslySetInnerHTML={
Object {
"__html": "&times; 2 nights",
}
} />
</span>
</div>
<div
@ -161,7 +170,7 @@ exports[`BookingBreakdown provider transaction data matches snapshot 1`] = `
</span>
<span
className={undefined}>
-2
2
</span>
</div>
<hr

View file

@ -10,14 +10,13 @@ import css from './OrderDetailsPanel.css';
const breakdown = transaction => {
const tx = ensureTransaction(transaction);
const listing = ensureListing(tx.listing);
const booking = ensureBooking(tx.booking);
const bookingStart = booking.attributes.start;
const bookingEnd = booking.attributes.end;
const unitPrice = listing.attributes.price;
const totalPrice = tx.attributes.total;
const payinTotal = tx.attributes.payinTotal;
const lineItems = tx.attributes.lineItems;
if (!bookingStart || !bookingEnd || !unitPrice || !totalPrice) {
if (!bookingStart || !bookingEnd || !payinTotal || !lineItems) {
return null;
}
return (
@ -25,8 +24,9 @@ const breakdown = transaction => {
className={css.receipt}
bookingStart={bookingStart}
bookingEnd={bookingEnd}
unitPrice={unitPrice}
totalPrice={totalPrice}
payinTotal={payinTotal}
lineItems={lineItems}
userRole="customer"
/>
);
};

View file

@ -42,6 +42,6 @@ describe('OrderDetailsPanel', () => {
});
const panel = shallow(<OrderDetailsPanel transaction={tx} />);
const breakdownProps = panel.find(BookingBreakdown).props();
expect(breakdownProps.totalPrice).toEqual(new Money(16500, 'USD'));
expect(breakdownProps.payinTotal).toEqual(new Money(16500, 'USD'));
});
});

View file

@ -47,17 +47,39 @@ exports[`OrderDetailsPanel matches snapshot 1`] = `
<BookingBreakdown
bookingEnd={2017-06-13T00:00:00.000Z}
bookingStart={2017-06-10T00:00:00.000Z}
totalPrice={
lineItems={
Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
"amount": 16500,
"currency": "USD",
},
"quantity": "3",
"unitPrice": Money {
"amount": 5500,
"currency": "USD",
},
},
Object {
"code": "line-item/provider-commission",
"lineTotal": Money {
"amount": -100,
"currency": "USD",
},
"unitPrice": Money {
"amount": -100,
"currency": "USD",
},
},
]
}
payinTotal={
Money {
"amount": 16500,
"currency": "USD",
}
}
unitPrice={
Money {
"amount": 5500,
"currency": "USD",
}
} />
userRole="customer" />
</div>
`;

View file

@ -1,59 +1,35 @@
import React, { PropTypes } from 'react';
import { FormattedDate, FormattedMessage } from 'react-intl';
import Decimal from 'decimal.js';
import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import { types } from '../../util/sdkLoader';
import { createSlug } from '../../util/urlHelpers';
import { ensureListing, ensureTransaction, ensureBooking, ensureUser } from '../../util/data';
import { convertMoneyToNumber, convertUnitToSubUnit } from '../../util/currency';
import config from '../../config';
import { Avatar, BookingBreakdown, NamedLink } from '../../components';
import css from './SaleDetailsPanel.css';
// TODO: This is a temporary function to calculate the booking
// price. This should be removed when the API supports dry-runs and we
// can take the total price from the transaction itself.
const estimatedProviderTotalPrice = (customerTotalPrice, commission) => {
const { subUnitDivisor, currency } = config.currencyConfig;
if (customerTotalPrice.currency !== currency || commission.currency !== currency) {
throw new Error('Transaction total or commission currency does not match marketplace currency');
}
const numericCustomerTotalPrice = convertMoneyToNumber(customerTotalPrice, subUnitDivisor);
const numericCommission = convertMoneyToNumber(commission, subUnitDivisor);
const numericTotalPrice = new Decimal(numericCustomerTotalPrice)
.minus(numericCommission)
.toNumber();
return new types.Money(convertUnitToSubUnit(numericTotalPrice, subUnitDivisor), currency);
};
const breakdown = transaction => {
const tx = ensureTransaction(transaction);
const listing = ensureListing(tx.listing);
const booking = ensureBooking(tx.booking);
const bookingStart = booking.attributes.start;
const bookingEnd = booking.attributes.end;
const unitPrice = listing.attributes.price;
const customerTotalPrice = tx.attributes.total;
const commission = tx.attributes.commission;
const payinTotal = tx.attributes.payinTotal;
const payoutTotal = tx.attributes.payoutTotal;
const lineItems = tx.attributes.lineItems;
if (!bookingStart || !bookingEnd || !unitPrice || !customerTotalPrice || !commission) {
if (!bookingStart || !bookingEnd || !payinTotal || !payoutTotal || !lineItems) {
return null;
}
const totalPrice = estimatedProviderTotalPrice(customerTotalPrice, commission);
return (
<BookingBreakdown
className={css.breakdown}
bookingStart={bookingStart}
bookingEnd={bookingEnd}
unitPrice={unitPrice}
totalPrice={totalPrice}
commission={commission}
payinTotal={payinTotal}
payoutTotal={payoutTotal}
lineItems={lineItems}
userRole="provider"
/>
);
};

View file

@ -46,6 +46,6 @@ describe('SaleDetailsPanel', () => {
const breakdownProps = panel.find(BookingBreakdown).props();
// Total price for the provider should be transaction total minus the commission.
expect(breakdownProps.totalPrice).toEqual(new Money(15500, 'USD'));
expect(breakdownProps.payoutTotal).toEqual(new Money(15500, 'USD'));
});
});

View file

@ -46,23 +46,45 @@ exports[`SaleDetailsPanel matches snapshot 1`] = `
<BookingBreakdown
bookingEnd={2017-06-13T00:00:00.000Z}
bookingStart={2017-06-10T00:00:00.000Z}
commission={
lineItems={
Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
"amount": 16500,
"currency": "USD",
},
"quantity": "3",
"unitPrice": Money {
"amount": 5500,
"currency": "USD",
},
},
Object {
"code": "line-item/provider-commission",
"lineTotal": Money {
"amount": -1000,
"currency": "USD",
},
"unitPrice": Money {
"amount": -1000,
"currency": "USD",
},
},
]
}
payinTotal={
Money {
"amount": 1000,
"amount": 16500,
"currency": "USD",
}
}
totalPrice={
payoutTotal={
Money {
"amount": 15500,
"currency": "USD",
}
}
unitPrice={
Money {
"amount": 5500,
"currency": "USD",
}
} />
userRole="provider" />
</div>
`;

View file

@ -18,10 +18,9 @@ import css from './BookingDatesForm.css';
// TODO: This is a temporary function to calculate the booking
// price. This should be removed when the API supports dry-runs and we
// can take the total price from the transaction itself.
const estimatedTotalPrice = (startDate, endDate, unitPrice) => {
const estimatedTotalPrice = (unitPrice, nightCount) => {
const { subUnitDivisor } = config.currencyConfig;
const numericPrice = convertMoneyToNumber(unitPrice, subUnitDivisor);
const nightCount = nightsBetween(startDate, endDate);
const numericTotalPrice = new Decimal(numericPrice).times(nightCount).toNumber();
return new types.Money(
convertUnitToSubUnit(numericTotalPrice, subUnitDivisor),
@ -33,14 +32,26 @@ const breakdown = (bookingStart, bookingEnd, unitPrice) => {
if (!bookingStart || !bookingEnd || !unitPrice) {
return null;
}
const totalPrice = estimatedTotalPrice(bookingStart, bookingEnd, unitPrice);
const nightCount = nightsBetween(bookingStart, bookingEnd);
const totalPrice = estimatedTotalPrice(unitPrice, nightCount);
const lineItems = [
{
code: 'line-item.purchase/night',
unitPrice: unitPrice,
quantity: new Decimal(nightCount),
lineTotal: totalPrice,
},
];
return (
<BookingBreakdown
className={css.receipt}
bookingStart={bookingStart}
bookingEnd={bookingEnd}
unitPrice={unitPrice}
totalPrice={totalPrice}
userRole="customer"
payinTotal={totalPrice}
lineItems={lineItems}
/>
);
};
@ -56,6 +67,8 @@ export const BookingDatesFormComponent = props => {
price: unitPrice,
submitting,
intl,
startDatePlaceholder,
endDatePlaceholder,
} = props;
const { startDate, endDate } = bookingDates;
@ -97,6 +110,10 @@ export const BookingDatesFormComponent = props => {
// https://momentjs.com/
const dateFormatString = 'ddd, MMMM D';
const startDatePlaceholderText = startDatePlaceholder || moment().format(dateFormatString);
const endDatePlaceholderText = endDatePlaceholder ||
moment().add(1, 'days').format(dateFormatString);
return (
<form className={className} onSubmit={handleSubmit}>
<DateRangeInputField
@ -104,10 +121,10 @@ export const BookingDatesFormComponent = props => {
name="bookingDates"
startDateId={`${form}.bookingStartDate`}
startDateLabel={bookingStartLabel}
startDatePlaceholderText={moment().format(dateFormatString)}
startDatePlaceholderText={startDatePlaceholderText}
endDateId={`${form}.bookingEndDate`}
endDateLabel={bookingEndLabel}
endDatePlaceholderText={moment().add(1, 'days').format(dateFormatString)}
endDatePlaceholderText={endDatePlaceholderText}
format={null}
useMobileMargins
validate={[
@ -130,6 +147,8 @@ BookingDatesFormComponent.defaultProps = {
rootClassName: null,
className: null,
price: null,
startDatePlaceholder: null,
endDatePlaceholder: null,
};
const { instanceOf, shape, string } = PropTypes;
@ -149,6 +168,10 @@ BookingDatesFormComponent.propTypes = {
// from inejctIntl
intl: intlShape.isRequired,
// for tests
startDatePlaceholder: string,
endDatePlaceholder: string,
};
const formName = 'BookingDates';

View file

@ -16,6 +16,8 @@ describe('BookingDatesForm', () => {
onSubmit={v => v}
price={new types.Money(1099, 'USD')}
bookingDates={{}}
startDatePlaceholder="today"
endDatePlaceholder="tomorrow"
/>
);
expect(tree).toMatchSnapshot();
@ -32,6 +34,8 @@ describe('BookingDatesForm', () => {
startDate: new Date(Date.UTC(2017, 3, 14)),
endDate: new Date(Date.UTC(2017, 3, 16)),
}}
startDatePlaceholder="today"
endDatePlaceholder="tomorrow"
/>
);
expect(tree).toMatchSnapshot();

View file

@ -5,12 +5,12 @@ exports[`BookingDatesForm matches snapshot with selected dates 1`] = `
<DateRangeInputField
endDateId="fakeTestForm.bookingEndDate"
endDateLabel="BookingDatesForm.bookingEndTitle"
endDatePlaceholderText="Sat, July 1"
endDatePlaceholderText="tomorrow"
format={null}
name="bookingDates"
startDateId="fakeTestForm.bookingStartDate"
startDateLabel="BookingDatesForm.bookingStartTitle"
startDatePlaceholderText="Fri, June 30"
startDatePlaceholderText="today"
useMobileMargins={true}
validate={
Array [
@ -21,7 +21,23 @@ exports[`BookingDatesForm matches snapshot with selected dates 1`] = `
<BookingBreakdown
bookingEnd={2017-04-16T00:00:00.000Z}
bookingStart={2017-04-14T00:00:00.000Z}
totalPrice={
lineItems={
Array [
Object {
"code": "line-item.purchase/night",
"lineTotal": Money {
"amount": 2198,
"currency": "USD",
},
"quantity": "2",
"unitPrice": Money {
"amount": 1099,
"currency": "USD",
},
},
]
}
payinTotal={
Money {
"amount": 2198,
"currency": "USD",
@ -32,7 +48,8 @@ exports[`BookingDatesForm matches snapshot with selected dates 1`] = `
"amount": 1099,
"currency": "USD",
}
} />
}
userRole="customer" />
<p>
<FormattedMessage
id="BookingDatesForm.youWontBeChargedInfo"
@ -57,12 +74,12 @@ exports[`BookingDatesForm matches snapshot without selected dates 1`] = `
<DateRangeInputField
endDateId="fakeTestForm.bookingEndDate"
endDateLabel="BookingDatesForm.bookingEndTitle"
endDatePlaceholderText="Sat, July 1"
endDatePlaceholderText="tomorrow"
format={null}
name="bookingDates"
startDateId="fakeTestForm.bookingStartDate"
startDateLabel="BookingDatesForm.bookingStartTitle"
startDatePlaceholderText="Fri, June 30"
startDatePlaceholderText="today"
useMobileMargins={true}
validate={
Array [

View file

@ -50,17 +50,42 @@ exports[`InboxPage matches snapshot 1`] = `
tx={
Object {
"attributes": Object {
"commission": Money {
"amount": 100,
"currency": "USD",
},
"createdAt": 2017-05-01T00:00:00.000Z,
"lastTransitionedAt": 2017-01-15T00:00:00.000Z,
"state": "state/preauthorized",
"total": Money {
"lineItems": Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
"amount": 1000,
"currency": "USD",
},
"quantity": "1",
"unitPrice": Money {
"amount": 1000,
"currency": "USD",
},
},
Object {
"code": "line-item/provider-commission",
"lineTotal": Money {
"amount": -100,
"currency": "USD",
},
"unitPrice": Money {
"amount": -100,
"currency": "USD",
},
},
],
"payinTotal": Money {
"amount": 1000,
"currency": "USD",
},
"payoutTotal": Money {
"amount": 900,
"currency": "USD",
},
"state": "state/preauthorized",
},
"booking": null,
"customer": null,
@ -102,17 +127,42 @@ exports[`InboxPage matches snapshot 1`] = `
tx={
Object {
"attributes": Object {
"commission": Money {
"amount": 100,
"currency": "USD",
},
"createdAt": 2017-05-01T00:00:00.000Z,
"lastTransitionedAt": 2016-01-15T00:00:00.000Z,
"state": "state/preauthorized",
"total": Money {
"lineItems": Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
"amount": 1000,
"currency": "USD",
},
"quantity": "1",
"unitPrice": Money {
"amount": 1000,
"currency": "USD",
},
},
Object {
"code": "line-item/provider-commission",
"lineTotal": Money {
"amount": -100,
"currency": "USD",
},
"unitPrice": Money {
"amount": -100,
"currency": "USD",
},
},
],
"payinTotal": Money {
"amount": 1000,
"currency": "USD",
},
"payoutTotal": Money {
"amount": 900,
"currency": "USD",
},
"state": "state/preauthorized",
},
"booking": null,
"customer": null,
@ -231,17 +281,42 @@ exports[`InboxPage matches snapshot 3`] = `
tx={
Object {
"attributes": Object {
"commission": Money {
"amount": 100,
"currency": "USD",
},
"createdAt": 2017-05-01T00:00:00.000Z,
"lastTransitionedAt": 2017-01-15T00:00:00.000Z,
"state": "state/preauthorized",
"total": Money {
"lineItems": Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
"amount": 1000,
"currency": "USD",
},
"quantity": "1",
"unitPrice": Money {
"amount": 1000,
"currency": "USD",
},
},
Object {
"code": "line-item/provider-commission",
"lineTotal": Money {
"amount": -100,
"currency": "USD",
},
"unitPrice": Money {
"amount": -100,
"currency": "USD",
},
},
],
"payinTotal": Money {
"amount": 1000,
"currency": "USD",
},
"payoutTotal": Money {
"amount": 900,
"currency": "USD",
},
"state": "state/preauthorized",
},
"booking": null,
"customer": Object {
@ -283,17 +358,42 @@ exports[`InboxPage matches snapshot 3`] = `
tx={
Object {
"attributes": Object {
"commission": Money {
"amount": 100,
"currency": "USD",
},
"createdAt": 2017-05-01T00:00:00.000Z,
"lastTransitionedAt": 2016-01-15T00:00:00.000Z,
"state": "state/preauthorized",
"total": Money {
"lineItems": Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
"amount": 1000,
"currency": "USD",
},
"quantity": "1",
"unitPrice": Money {
"amount": 1000,
"currency": "USD",
},
},
Object {
"code": "line-item/provider-commission",
"lineTotal": Money {
"amount": -100,
"currency": "USD",
},
"unitPrice": Money {
"amount": -100,
"currency": "USD",
},
},
],
"payinTotal": Money {
"amount": 1000,
"currency": "USD",
},
"payoutTotal": Money {
"amount": 900,
"currency": "USD",
},
"state": "state/preauthorized",
},
"booking": null,
"customer": Object {

View file

@ -8,17 +8,42 @@ exports[`OrderPage matches snapshot 1`] = `
transaction={
Object {
"attributes": Object {
"commission": Money {
"amount": 100,
"currency": "USD",
},
"createdAt": 2017-05-01T00:00:00.000Z,
"lastTransitionedAt": 2017-06-01T00:00:00.000Z,
"state": "state/preauthorized",
"total": Money {
"lineItems": Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
"amount": 1000,
"currency": "USD",
},
"quantity": "3",
"unitPrice": Money {
"amount": 333.3333333333333,
"currency": "USD",
},
},
Object {
"code": "line-item/provider-commission",
"lineTotal": Money {
"amount": -100,
"currency": "USD",
},
"unitPrice": Money {
"amount": -100,
"currency": "USD",
},
},
],
"payinTotal": Money {
"amount": 1000,
"currency": "USD",
},
"payoutTotal": Money {
"amount": 900,
"currency": "USD",
},
"state": "state/preauthorized",
},
"booking": Object {
"attributes": Object {

View file

@ -9,17 +9,42 @@ exports[`SalePage matches snapshot 1`] = `
transaction={
Object {
"attributes": Object {
"commission": Money {
"amount": 100,
"currency": "USD",
},
"createdAt": 2017-05-01T00:00:00.000Z,
"lastTransitionedAt": 2017-06-01T00:00:00.000Z,
"state": "state/preauthorized",
"total": Money {
"lineItems": Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
"amount": 1000,
"currency": "USD",
},
"quantity": "3",
"unitPrice": Money {
"amount": 333.3333333333333,
"currency": "USD",
},
},
Object {
"code": "line-item/provider-commission",
"lineTotal": Money {
"amount": -100,
"currency": "USD",
},
"unitPrice": Money {
"amount": -100,
"currency": "USD",
},
},
],
"payinTotal": Money {
"amount": 1000,
"currency": "USD",
},
"payoutTotal": Money {
"amount": 900,
"currency": "USD",
},
"state": "state/preauthorized",
},
"booking": Object {
"attributes": Object {

View file

@ -13,6 +13,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Decimal from 'decimal.js';
import { createInstance, types } from './util/sdkLoader';
import { ClientApp, renderApp } from './app';
import configureStore from './store';
@ -25,6 +26,8 @@ import routeConfiguration from './routesConfiguration';
import './marketplace.css';
const { BigDecimal } = types;
const render = store => {
// If the server already loaded the auth information, render the app
// immediately. Otherwise wait for the flag to be loaded and render
@ -54,7 +57,18 @@ if (typeof window !== 'undefined') {
// eslint-disable-next-line no-underscore-dangle
const preloadedState = window.__PRELOADED_STATE__ || '{}';
const initialState = JSON.parse(preloadedState, types.reviver);
const sdk = createInstance({ clientId: config.sdk.clientId, baseUrl: config.sdk.baseUrl });
const sdk = createInstance({
clientId: config.sdk.clientId,
baseUrl: config.sdk.baseUrl,
typeHandlers: [
{
type: BigDecimal,
customType: Decimal,
writer: v => new BigDecimal(v.toString()),
reader: v => new Decimal(v.value),
},
],
});
const store = configureStore(sdk, initialState);
setupStripe();

View file

@ -10,7 +10,7 @@
"AuthorInfo.host": "Hosted by {firstName} {lastName}",
"BookingBreakdown.bookingPeriod": "{bookingStart} - {bookingEnd}",
"BookingBreakdown.commission": "Saunatime fee:",
"BookingBreakdown.nightCount": "* {count, number} {count, plural, one {night} other {nights}}",
"BookingBreakdown.nightCount": "&times; {count, number} {count, plural, one {night} other {nights}}",
"BookingBreakdown.pricePerNight": "Price per night",
"BookingBreakdown.total": "Total price",
"BookingDatesForm.bookingEndTitle": "End date",

View file

@ -147,12 +147,12 @@ export const transaction = shape({
id: uuid.isRequired,
type: value('transaction').isRequired,
attributes: shape({
commission: money.isRequired,
createdAt: instanceOf(Date).isRequired,
lastTransitionedAt: instanceOf(Date).isRequired,
lastTransition: string,
state: oneOf(TX_STATES).isRequired,
total: money.isRequired,
payinTotal: money.isRequired,
payoutTotal: money.isRequired,
}),
booking,
listing,

View file

@ -1,4 +1,6 @@
import Decimal from 'decimal.js';
import { types } from './sdkLoader';
import { nightsBetween } from '../util/dates';
const { UUID, LatLng, Money } = types;
@ -80,15 +82,29 @@ export const createTransaction = options => {
provider = null,
lastTransitionedAt = new Date(Date.UTC(2017, 5, 1)),
} = options;
const nightCount = booking ? nightsBetween(booking.attributes.start, booking.attributes.end) : 1;
return {
id: new UUID(id),
type: 'transaction',
attributes: {
commission,
createdAt: new Date(Date.UTC(2017, 4, 1)),
lastTransitionedAt,
state,
total,
payinTotal: total,
payoutTotal: new Money(total.amount - commission.amount, total.currency),
lineItems: [
{
code: 'line-item/night',
quantity: new Decimal(nightCount),
unitPrice: new Money(total.amount / nightCount, total.currency),
lineTotal: total,
},
{
code: 'line-item/provider-commission',
unitPrice: new Money(commission.amount * -1, commission.currency),
lineTotal: new Money(commission.amount * -1, commission.currency),
},
],
},
booking,
listing,

View file

@ -6008,9 +6008,9 @@ sharetribe-scripts@0.9.2:
optionalDependencies:
fsevents "1.0.17"
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#a3d2bfcc64e0b91c6626b959c8395fbda39fd0d8":
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#a086f37c765cf5b6034bc5f4788788237485c863":
version "0.0.1"
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#a3d2bfcc64e0b91c6626b959c8395fbda39fd0d8"
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#a086f37c765cf5b6034bc5f4788788237485c863"
dependencies:
axios "^0.15.3"
js-cookie "^2.1.3"