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.js b/src/components/BookingBreakdown/BookingBreakdown.js index a6e0da1d..0a3bf9ca 100644 --- a/src/components/BookingBreakdown/BookingBreakdown.js +++ b/src/components/BookingBreakdown/BookingBreakdown.js @@ -2,6 +2,7 @@ * This component will show the booking info and calculated total price. * I.e. dates and other details related to payment decision in receipt format. */ +import _ from 'lodash'; import React, { PropTypes } from 'react'; import { FormattedMessage, intlShape, injectIntl } from 'react-intl'; import Decimal from 'decimal.js'; @@ -9,7 +10,6 @@ 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,9 +19,10 @@ export const BookingBreakdownComponent = props => { className, bookingStart, bookingEnd, - unitPrice, - commission, - totalPrice, + payinTotal, + payoutTotal, + lineItems, + userRole, intl, } = props; @@ -50,7 +51,13 @@ export const BookingBreakdownComponent = props => { /> ); - const nightCount = nightsBetween(bookingStart, bookingEnd); + const nightPurchase = _.find(lineItems, item => item.code === 'line-item.purchase/night'); + const providerCommission = _.find( + lineItems, + item => item.code === 'line-item.commission/provider' + ); + + const nightCount = nightPurchase.quantity.toFixed(); const nightCountMessage = ( ); @@ -58,10 +65,11 @@ export const BookingBreakdownComponent = props => { 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 commission = providerCommission.lineTotal; const commissionAsNumber = commission ? convertMoneyToNumber(commission, subUnitDivisor) : 0; const formattedCommission = commission ? intl.formatNumber(new Decimal(commissionAsNumber).negated().toNumber(), currencyConfig) @@ -76,7 +84,10 @@ export const BookingBreakdownComponent = props => { ); - const totalPriceAsNumber = convertMoneyToNumber(totalPrice, subUnitDivisor); + const totalPriceAsNumber = convertMoneyToNumber( + userRole === 'customer' ? payinTotal : payoutTotal, + subUnitDivisor + ); const formattedTotalPrice = totalPriceAsNumber ? intl.formatNumber(totalPriceAsNumber, currencyConfig) : null; @@ -95,7 +106,7 @@ export const BookingBreakdownComponent = props => { {nightCountMessage} - {commission ? commissionInfo : null} + {userRole === 'provider' ? commissionInfo : null}
@@ -115,7 +126,14 @@ BookingBreakdownComponent.defaultProps = { commission: 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 +141,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.isRequired, + payoutTotal: propTypes.money.isRequired, // from injectIntl intl: intlShape.isRequired, diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.js index 34c5b315..4f0006c5 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.js +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.js @@ -10,14 +10,14 @@ 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.payinTotal; + const payinTotal = tx.attributes.payinTotal; + const payoutTotal = tx.attributes.payoutTotal; + const lineItems = tx.attributes.lineItems; - if (!bookingStart || !bookingEnd || !unitPrice || !totalPrice) { + if (!bookingStart || !bookingEnd || !payinTotal || !payoutTotal || !lineItems) { return null; } return ( @@ -25,8 +25,10 @@ const breakdown = transaction => { className={css.receipt} bookingStart={bookingStart} bookingEnd={bookingEnd} - unitPrice={unitPrice} - totalPrice={totalPrice} + payinTotal={payinTotal} + payoutTotal={payoutTotal} + lineItems={lineItems} + userRole="customer" /> ); }; diff --git a/src/components/SaleDetailsPanel/SaleDetailsPanel.js b/src/components/SaleDetailsPanel/SaleDetailsPanel.js index 6a6968d4..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 estimatedCommission = (customerTotalPrice, providerTotalPrice) => { - const { subUnitDivisor, currency } = config.currencyConfig; - if (customerTotalPrice.currency !== currency || providerTotalPrice.currency !== currency) { - throw new Error('Transaction total or commission currency does not match marketplace currency'); - } - - const numericCustomerTotalPrice = convertMoneyToNumber(customerTotalPrice, subUnitDivisor); - const numericProviderTotalPrice = convertMoneyToNumber(providerTotalPrice, subUnitDivisor); - const numericCommission = new Decimal(numericProviderTotalPrice) - .minus(numericCustomerTotalPrice) - .toNumber(); - - return new types.Money(convertUnitToSubUnit(numericCommission, 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.payinTotal; - const providerTotalPrice = tx.attributes.payoutTotal; + const payinTotal = tx.attributes.payinTotal; + const payoutTotal = tx.attributes.payoutTotal; + const lineItems = tx.attributes.lineItems; - if (!bookingStart || !bookingEnd || !unitPrice || !customerTotalPrice || !providerTotalPrice) { + if (!bookingStart || !bookingEnd || !payinTotal || !payoutTotal || !lineItems) { return null; } - const commission = estimatedCommission(customerTotalPrice, providerTotalPrice); - return ( ); }; diff --git a/src/index.js b/src/index.js index a82c6210..b4fc1b9a 100644 --- a/src/index.js +++ b/src/index.js @@ -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();