diff --git a/package.json b/package.json
index bcf33cc6..d173c9fa 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/server/index.js b/server/index.js
index 5618d8ad..a09e62ff 100644
--- a/server/index.js
+++ b/server/index.js
@@ -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
diff --git a/src/components/BookingBreakdown/BookingBreakdown.example.js b/src/components/BookingBreakdown/BookingBreakdown.example.js
index bcc06026..d578b693 100644
--- a/src/components/BookingBreakdown/BookingBreakdown.example.js
+++ b/src/components/BookingBreakdown/BookingBreakdown.example.js
@@ -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'),
},
};
diff --git a/src/components/BookingBreakdown/BookingBreakdown.js b/src/components/BookingBreakdown/BookingBreakdown.js
index a6e0da1d..4f30bdfb 100644
--- a/src/components/BookingBreakdown/BookingBreakdown.js
+++ b/src/components/BookingBreakdown/BookingBreakdown.js
@@ -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 = (
-
+
);
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 = (
-
-
-
-
- {formattedCommission}
-
+ 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 = (
+
+
+
+
+ {formattedCommission}
+
+ );
+ }
+
+ 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 => {
{nightCountMessage}
- {commission ? commissionInfo : null}
+ {commissionInfo}
@@ -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,
diff --git a/src/components/BookingBreakdown/BookingBreakdown.test.js b/src/components/BookingBreakdown/BookingBreakdown.test.js
index b326c566..9d42041a 100644
--- a/src/components/BookingBreakdown/BookingBreakdown.test.js
+++ b/src/components/BookingBreakdown/BookingBreakdown.test.js
@@ -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', () => {
);
@@ -23,8 +32,16 @@ describe('BookingBreakdown', () => {
);
@@ -36,9 +53,22 @@ describe('BookingBreakdown', () => {
);
diff --git a/src/components/BookingBreakdown/__snapshots__/BookingBreakdown.test.js.snap b/src/components/BookingBreakdown/__snapshots__/BookingBreakdown.test.js.snap
index 6f0e15fa..2d22d38e 100644
--- a/src/components/BookingBreakdown/__snapshots__/BookingBreakdown.test.js.snap
+++ b/src/components/BookingBreakdown/__snapshots__/BookingBreakdown.test.js.snap
@@ -32,9 +32,12 @@ exports[`BookingBreakdown customer transaction data matches snapshot 1`] = `
-
- * 2 nights
-
+
-
- * 2 nights
-
+
-
- * 2 nights
-
+
- -2
+ 2
{
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"
/>
);
};
diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js
index e5b35643..bee691cc 100644
--- a/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js
+++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js
@@ -42,6 +42,6 @@ describe('OrderDetailsPanel', () => {
});
const panel = shallow();
const breakdownProps = panel.find(BookingBreakdown).props();
- expect(breakdownProps.totalPrice).toEqual(new Money(16500, 'USD'));
+ expect(breakdownProps.payinTotal).toEqual(new Money(16500, 'USD'));
});
});
diff --git a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap
index d0d51d71..37c2fe32 100644
--- a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap
+++ b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap
@@ -47,17 +47,39 @@ exports[`OrderDetailsPanel matches snapshot 1`] = `
+ userRole="customer" />
`;
diff --git a/src/components/SaleDetailsPanel/SaleDetailsPanel.js b/src/components/SaleDetailsPanel/SaleDetailsPanel.js
index eac2da77..8c0a740d 100644
--- a/src/components/SaleDetailsPanel/SaleDetailsPanel.js
+++ b/src/components/SaleDetailsPanel/SaleDetailsPanel.js
@@ -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 (
);
};
diff --git a/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js b/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js
index 10b6e88f..312f1ab1 100644
--- a/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js
+++ b/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js
@@ -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'));
});
});
diff --git a/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap b/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap
index b194fa7f..63bd0907 100644
--- a/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap
+++ b/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap
@@ -46,23 +46,45 @@ exports[`SaleDetailsPanel matches snapshot 1`] = `
+ userRole="provider" />
`;
diff --git a/src/containers/BookingDatesForm/BookingDatesForm.js b/src/containers/BookingDatesForm/BookingDatesForm.js
index 48a1d614..e7af8ce4 100644
--- a/src/containers/BookingDatesForm/BookingDatesForm.js
+++ b/src/containers/BookingDatesForm/BookingDatesForm.js
@@ -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 (
);
};
@@ -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 (