Use line items to show price breakdown

This commit is contained in:
Mikko Koski 2017-06-29 14:25:30 +03:00 committed by Vesa Luusua
parent 4364a4a7b2
commit f265c72a55
5 changed files with 71 additions and 52 deletions

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

@ -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 = (
<FormattedMessage id="BookingBreakdown.nightCount" values={{ count: nightCount }} />
);
@ -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 => {
</div>
);
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 => {
</span>
<span className={css.itemValue}>{nightCountMessage}</span>
</div>
{commission ? commissionInfo : null}
{userRole === 'provider' ? commissionInfo : null}
<hr className={css.totalDivider} />
<div className={css.lineItem}>
<div className={css.totalLabel}>
@ -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,

View file

@ -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"
/>
);
};

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 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 (
<BookingBreakdown
className={css.breakdown}
bookingStart={bookingStart}
bookingEnd={bookingEnd}
unitPrice={unitPrice}
totalPrice={providerTotalPrice}
commission={commission}
payinTotal={payinTotal}
payoutTotal={payoutTotal}
lineItems={lineItems}
userRole="provider"
/>
);
};

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();