Merge pull request #619 from sharetribe/daily-breakdown

Daily breakdown
This commit is contained in:
Kimmo Puputti 2018-01-04 11:46:22 +02:00 committed by GitHub
commit 8f401bc31a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 800 additions and 83 deletions

View file

@ -43,12 +43,14 @@ export const Checkout = {
component: BookingBreakdown,
props: {
userRole: 'customer',
unitType: propTypes.LINE_ITEM_NIGHT,
transaction: exampleTransaction({
payinTotal: new Money(9000, CURRENCY),
payoutTotal: new Money(9000, CURRENCY),
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(2),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(9000, CURRENCY),
@ -67,12 +69,14 @@ export const CustomerOrder = {
component: BookingBreakdown,
props: {
userRole: 'customer',
unitType: propTypes.LINE_ITEM_NIGHT,
transaction: exampleTransaction({
payinTotal: new Money(9000, CURRENCY),
payoutTotal: new Money(9000, CURRENCY),
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(2),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(9000, CURRENCY),
@ -91,12 +95,14 @@ export const ProviderSale = {
component: BookingBreakdown,
props: {
userRole: 'provider',
unitType: propTypes.LINE_ITEM_NIGHT,
transaction: exampleTransaction({
payinTotal: new Money(9000, CURRENCY),
payoutTotal: new Money(7000, CURRENCY),
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(2),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(9000, CURRENCY),
@ -104,6 +110,7 @@ export const ProviderSale = {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
unitPrice: new Money(-2000, CURRENCY),
lineTotal: new Money(-2000, CURRENCY),
reversal: false,
@ -121,12 +128,14 @@ export const ProviderSaleZeroCommission = {
component: BookingBreakdown,
props: {
userRole: 'provider',
unitType: propTypes.LINE_ITEM_NIGHT,
transaction: exampleTransaction({
payinTotal: new Money(9000, CURRENCY),
payoutTotal: new Money(9000, CURRENCY),
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(2),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(9000, CURRENCY),
@ -134,6 +143,7 @@ export const ProviderSaleZeroCommission = {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
unitPrice: new Money(0, CURRENCY),
lineTotal: new Money(0, CURRENCY),
reversal: false,
@ -151,12 +161,14 @@ export const ProviderSaleSingleNight = {
component: BookingBreakdown,
props: {
userRole: 'provider',
unitType: propTypes.LINE_ITEM_NIGHT,
transaction: exampleTransaction({
payinTotal: new Money(4500, CURRENCY),
payoutTotal: new Money(2500, CURRENCY),
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(1),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(4500, CURRENCY),
@ -164,6 +176,7 @@ export const ProviderSaleSingleNight = {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
unitPrice: new Money(-2000, CURRENCY),
lineTotal: new Money(-2000, CURRENCY),
reversal: false,
@ -181,6 +194,7 @@ export const ProviderSalePreauthorized = {
component: BookingBreakdown,
props: {
userRole: 'provider',
unitType: propTypes.LINE_ITEM_NIGHT,
transaction: exampleTransaction({
lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
payinTotal: new Money(4500, CURRENCY),
@ -188,6 +202,7 @@ export const ProviderSalePreauthorized = {
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(1),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(4500, CURRENCY),
@ -195,6 +210,7 @@ export const ProviderSalePreauthorized = {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
unitPrice: new Money(-2000, CURRENCY),
lineTotal: new Money(-2000, CURRENCY),
reversal: false,
@ -212,6 +228,7 @@ export const ProviderSaleAccepted = {
component: BookingBreakdown,
props: {
userRole: 'provider',
unitType: propTypes.LINE_ITEM_NIGHT,
transaction: exampleTransaction({
lastTransition: propTypes.TX_TRANSITION_ACCEPT,
payinTotal: new Money(4500, CURRENCY),
@ -219,6 +236,7 @@ export const ProviderSaleAccepted = {
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(1),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(4500, CURRENCY),
@ -226,6 +244,7 @@ export const ProviderSaleAccepted = {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
unitPrice: new Money(-2000, CURRENCY),
lineTotal: new Money(-2000, CURRENCY),
reversal: false,
@ -243,6 +262,7 @@ export const ProviderSaleDeclined = {
component: BookingBreakdown,
props: {
userRole: 'provider',
unitType: propTypes.LINE_ITEM_NIGHT,
transaction: exampleTransaction({
lastTransition: propTypes.TX_TRANSITION_DECLINE,
payinTotal: new Money(4500, CURRENCY),
@ -250,6 +270,7 @@ export const ProviderSaleDeclined = {
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(1),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(4500, CURRENCY),
@ -257,6 +278,7 @@ export const ProviderSaleDeclined = {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
unitPrice: new Money(-2000, CURRENCY),
lineTotal: new Money(-2000, CURRENCY),
reversal: false,
@ -274,6 +296,7 @@ export const ProviderSaleAutoDeclined = {
component: BookingBreakdown,
props: {
userRole: 'provider',
unitType: propTypes.LINE_ITEM_NIGHT,
transaction: exampleTransaction({
lastTransition: propTypes.TX_TRANSITION_AUTO_DECLINE,
payinTotal: new Money(4500, CURRENCY),
@ -281,6 +304,7 @@ export const ProviderSaleAutoDeclined = {
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(1),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(4500, CURRENCY),
@ -288,6 +312,7 @@ export const ProviderSaleAutoDeclined = {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
unitPrice: new Money(-2000, CURRENCY),
lineTotal: new Money(-2000, CURRENCY),
reversal: false,
@ -305,6 +330,7 @@ export const ProviderSaleDelivered = {
component: BookingBreakdown,
props: {
userRole: 'provider',
unitType: propTypes.LINE_ITEM_NIGHT,
transaction: exampleTransaction({
lastTransition: propTypes.TX_TRANSITION_MARK_DELIVERED,
payinTotal: new Money(4500, CURRENCY),
@ -312,6 +338,7 @@ export const ProviderSaleDelivered = {
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(1),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(4500, CURRENCY),
@ -319,6 +346,7 @@ export const ProviderSaleDelivered = {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
unitPrice: new Money(-2000, CURRENCY),
lineTotal: new Money(-2000, CURRENCY),
reversal: false,
@ -336,6 +364,7 @@ export const ProviderSaleCanceled = {
component: BookingBreakdown,
props: {
userRole: 'provider',
unitType: propTypes.LINE_ITEM_NIGHT,
transaction: exampleTransaction({
lastTransition: propTypes.TX_TRANSITION_CANCEL,
payinTotal: new Money(0, CURRENCY),
@ -343,6 +372,7 @@ export const ProviderSaleCanceled = {
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(1),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(4500, CURRENCY),
@ -350,6 +380,7 @@ export const ProviderSaleCanceled = {
},
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(-1),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(-4500, CURRENCY),
@ -357,6 +388,7 @@ export const ProviderSaleCanceled = {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
quantity: new Decimal(1),
unitPrice: new Money(-2000, CURRENCY),
lineTotal: new Money(-2000, CURRENCY),
@ -364,6 +396,7 @@ export const ProviderSaleCanceled = {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
quantity: new Decimal(-1),
unitPrice: new Money(2000, CURRENCY),
lineTotal: new Money(-2000, CURRENCY),
@ -377,3 +410,55 @@ export const ProviderSaleCanceled = {
}),
},
};
export const SingleDay = {
component: BookingBreakdown,
props: {
userRole: 'customer',
unitType: propTypes.LINE_ITEM_DAY,
transaction: exampleTransaction({
payinTotal: new Money(4500, CURRENCY),
payoutTotal: new Money(4500, CURRENCY),
lineItems: [
{
code: 'line-item/day',
includeFor: ['customer', 'provider'],
quantity: new Decimal(1),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(4500, CURRENCY),
reversal: false,
},
],
}),
booking: exampleBooking({
start: new Date(Date.UTC(2017, 3, 14)),
end: new Date(Date.UTC(2017, 3, 15)),
}),
},
};
export const MultipleDays = {
component: BookingBreakdown,
props: {
userRole: 'customer',
unitType: propTypes.LINE_ITEM_DAY,
transaction: exampleTransaction({
payinTotal: new Money(9000, CURRENCY),
payoutTotal: new Money(9000, CURRENCY),
lineItems: [
{
code: 'line-item/day',
includeFor: ['customer', 'provider'],
quantity: new Decimal(2),
unitPrice: new Money(4500, CURRENCY),
lineTotal: new Money(9000, CURRENCY),
reversal: false,
},
],
}),
booking: exampleBooking({
start: new Date(Date.UTC(2017, 3, 14)),
end: new Date(Date.UTC(2017, 3, 16)),
}),
},
};

View file

@ -3,12 +3,14 @@
* I.e. dates and other details related to payment decision in receipt format.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { bool, oneOf, string } from 'prop-types';
import { FormattedMessage, FormattedHTMLMessage, intlShape, injectIntl } from 'react-intl';
import moment from 'moment';
import classNames from 'classnames';
import { types } from '../../util/sdkLoader';
import { formatMoney } from '../../util/currency';
import * as propTypes from '../../util/propTypes';
import { daysBetween } from '../../util/dates';
import css from './BookingBreakdown.css';
@ -24,73 +26,103 @@ const isValidCommission = commissionLineItem => {
);
};
export const BookingBreakdownComponent = props => {
const { rootClassName, className, userRole, transaction, booking, intl } = props;
const UnitPriceItem = props => {
const { transaction, unitType, intl } = props;
const isNightly = unitType === propTypes.LINE_ITEM_NIGHT;
const unitPurchase = transaction.attributes.lineItems.find(
item => item.code === unitType && !item.reversal
);
const formattedUnitPrice = formatMoney(intl, unitPurchase.unitPrice);
const isProvider = userRole === 'provider';
const classes = classNames(rootClassName || css.root, className);
return (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage
id={isNightly ? 'BookingBreakdown.pricePerNight' : 'BookingBreakdown.pricePerDay'}
/>
</span>
<span className={css.itemValue}>{formattedUnitPrice}</span>
</div>
);
};
UnitPriceItem.propTypes = {
transaction: propTypes.transaction.isRequired,
unitType: oneOf([propTypes.LINE_ITEM_NIGHT, propTypes.LINE_ITEM_DAY]).isRequired,
intl: intlShape.isRequired,
};
const UnitsItem = props => {
const { transaction, booking, unitType, intl } = props;
const { start: startDate, end: endDateRaw } = booking.attributes;
const isNightly = unitType === propTypes.LINE_ITEM_NIGHT;
const isSingleDay = !isNightly && daysBetween(startDate, endDateRaw) === 1;
const endDay = isNightly ? endDateRaw : moment(endDateRaw).subtract(1, 'days');
const dateFormatOptions = {
weekday: 'short',
month: 'short',
day: 'numeric',
};
const bookingPeriod = (
const bookingPeriod = isSingleDay ? (
intl.formatDate(startDate, dateFormatOptions)
) : (
<FormattedMessage
id="BookingBreakdown.bookingPeriod"
values={{
bookingStart: (
<span className={css.nowrap}>
{intl.formatDate(booking.attributes.start, dateFormatOptions)}
</span>
<span className={css.nowrap}>{intl.formatDate(startDate, dateFormatOptions)}</span>
),
bookingEnd: (
<span className={css.nowrap}>
{intl.formatDate(booking.attributes.end, dateFormatOptions)}
</span>
<span className={css.nowrap}>{intl.formatDate(endDay, dateFormatOptions)}</span>
),
}}
/>
);
const nightPurchase = transaction.attributes.lineItems.find(
item => item.code === 'line-item/night' && !item.reversal
const unitPurchase = transaction.attributes.lineItems.find(
item => item.code === unitType && !item.reversal
);
const providerCommissionLineItem = transaction.attributes.lineItems.find(
item => item.code === 'line-item/provider-commission' && !item.reversal
);
const refund = transaction.attributes.lineItems.find(
item => item.code === 'line-item/night' && item.reversal
);
const commissionRefund = transaction.attributes.lineItems.find(
item => item.code === 'line-item/provider-commission' && item.reversal
const unitCount = unitPurchase.quantity.toFixed();
const unitCountMessage = (
<FormattedHTMLMessage
id={isNightly ? 'BookingBreakdown.nightCount' : 'BookingBreakdown.dayCount'}
values={{ count: unitCount }}
/>
);
const refundInfo = refund ? (
return (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.refund" />
</span>
<span className={css.itemValue}>{formatMoney(intl, refund.lineTotal)}</span>
<span className={css.itemLabel}>{bookingPeriod}</span>
<span className={css.itemValue}>{unitCountMessage}</span>
</div>
) : null;
);
};
const nightCount = nightPurchase.quantity.toFixed();
const nightCountMessage = (
<FormattedHTMLMessage id="BookingBreakdown.nightCount" values={{ count: nightCount }} />
UnitsItem.propTypes = {
transaction: propTypes.transaction.isRequired,
booking: propTypes.booking.isRequired,
intl: intlShape.isRequired,
};
const SubTotalItemMaybe = props => {
const { transaction, unitType, isProvider, intl } = props;
const refund = transaction.attributes.lineItems.find(
item => item.code === unitType && item.reversal
);
const formattedUnitPrice = formatMoney(intl, nightPurchase.unitPrice);
// If commission is passed it will be shown as a fee already reduces from the total price
let commissionInfo = null;
// Show night purchase line total (unit price * quantity) as a subtotal.
// Show unit purchase line total (unit price * quantity) as a subtotal.
// PLEASE NOTE that this assumes that the transaction doesn't have other
// line item types than nights (e.g. week, month, year).
// line item types than the defined unit type (e.g. week, month, year).
const showSubTotal = isProvider || refund;
const formattedSubTotal = formatMoney(intl, nightPurchase.lineTotal);
const subTotalInfo = showSubTotal ? (
const unitPurchase = transaction.attributes.lineItems.find(
item => item.code === unitType && !item.reversal
);
const formattedSubTotal = formatMoney(intl, unitPurchase.lineTotal);
return showSubTotal ? (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.subTotal" />
@ -98,6 +130,26 @@ export const BookingBreakdownComponent = props => {
<span className={css.itemValue}>{formattedSubTotal}</span>
</div>
) : null;
};
SubTotalItemMaybe.propTypes = {
transaction: propTypes.transaction.isRequired,
isProvider: bool.isRequired,
intl: intlShape.isRequired,
};
const CommissionItemMaybe = props => {
const { transaction, isProvider, intl } = props;
const providerCommissionLineItem = transaction.attributes.lineItems.find(
item => item.code === propTypes.LINE_ITEM_PROVIDER_COMMISSION && !item.reversal
);
const commissionRefund = transaction.attributes.lineItems.find(
item => item.code === propTypes.LINE_ITEM_PROVIDER_COMMISSION && item.reversal
);
// If commission is passed it will be shown as a fee already reduces from the total price
let commissionItem = null;
if (isProvider) {
if (!isValidCommission(providerCommissionLineItem)) {
@ -109,7 +161,7 @@ export const BookingBreakdownComponent = props => {
const commission = providerCommissionLineItem.lineTotal;
const formattedCommission = commission ? formatMoney(intl, commission) : null;
commissionInfo = commissionRefund ? null : (
commissionItem = commissionRefund ? null : (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.commission" />
@ -119,6 +171,43 @@ export const BookingBreakdownComponent = props => {
);
}
return commissionItem;
};
CommissionItemMaybe.propTypes = {
transaction: propTypes.transaction.isRequired,
isProvider: bool.isRequired,
intl: intlShape.isRequired,
};
const RefundItemMaybe = props => {
const { transaction, unitType, intl } = props;
const refund = transaction.attributes.lineItems.find(
item => item.code === unitType && item.reversal
);
return refund ? (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.refund" />
</span>
<span className={css.itemValue}>{formatMoney(intl, refund.lineTotal)}</span>
</div>
) : null;
};
RefundItemMaybe.propTypes = {
transaction: propTypes.transaction.isRequired,
intl: intlShape.isRequired,
};
export const BookingBreakdownComponent = props => {
const { rootClassName, className, userRole, unitType, transaction, booking, intl } = props;
const isProvider = userRole === 'provider';
const classes = classNames(rootClassName || css.root, className);
let providerTotalMessageId = 'BookingBreakdown.providerTotalDefault';
if (propTypes.txIsDelivered(transaction)) {
providerTotalMessageId = 'BookingBreakdown.providerTotalDelivered';
@ -141,19 +230,16 @@ export const BookingBreakdownComponent = props => {
return (
<div className={classes}>
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.pricePerNight" />
</span>
<span className={css.itemValue}>{formattedUnitPrice}</span>
</div>
<div className={css.lineItem}>
<span className={css.itemLabel}>{bookingPeriod}</span>
<span className={css.itemValue}>{nightCountMessage}</span>
</div>
{subTotalInfo}
{commissionInfo}
{refundInfo}
<UnitPriceItem transaction={transaction} unitType={unitType} intl={intl} />
<UnitsItem transaction={transaction} booking={booking} unitType={unitType} intl={intl} />
<SubTotalItemMaybe
transaction={transaction}
unitType={unitType}
isProvider={isProvider}
intl={intl}
/>
<CommissionItemMaybe transaction={transaction} isProvider={isProvider} intl={intl} />
<RefundItemMaybe transaction={transaction} unitType={unitType} intl={intl} />
<hr className={css.totalDivider} />
<div className={css.lineItem}>
<div className={css.totalLabel}>{totalLabel}</div>
@ -165,13 +251,12 @@ export const BookingBreakdownComponent = props => {
BookingBreakdownComponent.defaultProps = { rootClassName: null, className: null };
const { oneOf, string } = PropTypes;
BookingBreakdownComponent.propTypes = {
rootClassName: string,
className: string,
userRole: oneOf(['customer', 'provider']).isRequired,
unitType: oneOf([propTypes.LINE_ITEM_NIGHT, propTypes.LINE_ITEM_DAY]).isRequired,
transaction: propTypes.transaction.isRequired,
booking: propTypes.booking.isRequired,

View file

@ -36,12 +36,14 @@ describe('BookingBreakdown', () => {
const tree = renderDeep(
<BookingBreakdownComponent
userRole="customer"
unitType={propTypes.LINE_ITEM_NIGHT}
transaction={exampleTransaction({
payinTotal: new Money(2000, 'USD'),
payoutTotal: new Money(2000, 'USD'),
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(2),
lineTotal: new Money(2000, 'USD'),
unitPrice: new Money(1000, 'USD'),
@ -63,12 +65,14 @@ describe('BookingBreakdown', () => {
const tree = renderDeep(
<BookingBreakdownComponent
userRole="customer"
unitType={propTypes.LINE_ITEM_NIGHT}
transaction={exampleTransaction({
payinTotal: new Money(2000, 'USD'),
payoutTotal: new Money(2000, 'USD'),
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(2),
lineTotal: new Money(2000, 'USD'),
unitPrice: new Money(1000, 'USD'),
@ -90,12 +94,14 @@ describe('BookingBreakdown', () => {
const tree = renderDeep(
<BookingBreakdownComponent
userRole="provider"
unitType={propTypes.LINE_ITEM_NIGHT}
transaction={exampleTransaction({
payinTotal: new Money(2000, 'USD'),
payoutTotal: new Money(1800, 'USD'),
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(2),
lineTotal: new Money(2000, 'USD'),
unitPrice: new Money(1000, 'USD'),
@ -103,6 +109,7 @@ describe('BookingBreakdown', () => {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
lineTotal: new Money(-200, 'USD'),
unitPrice: new Money(-200, 'USD'),
reversal: false,
@ -122,6 +129,7 @@ describe('BookingBreakdown', () => {
const tree = renderDeep(
<BookingBreakdownComponent
userRole="provider"
unitType={propTypes.LINE_ITEM_NIGHT}
transaction={exampleTransaction({
lastTransition: propTypes.TX_TRANSITION_CANCEL,
payinTotal: new Money(0, 'USD'),
@ -129,6 +137,7 @@ describe('BookingBreakdown', () => {
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(2),
lineTotal: new Money(2000, 'USD'),
unitPrice: new Money(1000, 'USD'),
@ -136,6 +145,7 @@ describe('BookingBreakdown', () => {
},
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(-2),
lineTotal: new Money(-2000, 'USD'),
unitPrice: new Money(1000, 'USD'),
@ -143,6 +153,7 @@ describe('BookingBreakdown', () => {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
quantity: new Decimal(1),
lineTotal: new Money(-200, 'USD'),
unitPrice: new Money(-200, 'USD'),
@ -150,6 +161,7 @@ describe('BookingBreakdown', () => {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
quantity: new Decimal(-1),
lineTotal: new Money(200, 'USD'),
unitPrice: new Money(-200, 'USD'),

View file

@ -71,7 +71,7 @@
}
}
.perNight {
.perUnit {
/* Font */
@apply --marketplaceTinyFontStyles;
color: var(--matterColor);

View file

@ -64,8 +64,8 @@ export const ListingCardComponent = props => {
<div className={css.priceValue} title={priceTitle}>
{formattedPrice}
</div>
<div className={css.perNight}>
<FormattedMessage id="ListingCard.perNight" />
<div className={css.perUnit}>
<FormattedMessage id="ListingCard.perUnit" />
</div>
</div>
<div className={css.mainInfo}>

View file

@ -44,7 +44,7 @@ exports[`ListingCard matches snapshot 1`] = `
</div>
<div>
<FormattedMessage
id="ListingCard.perNight"
id="ListingCard.perUnit"
values={Object {}}
/>
</div>

View file

@ -220,7 +220,7 @@
}
}
.perNight {
.perUnit {
/* Font */
@apply --marketplaceTinyFontStyles;
color: var(--matterColor);

View file

@ -219,8 +219,8 @@ export const ManageListingCardComponent = props => {
<div className={css.priceValue} title={priceTitle}>
{formattedPrice}
</div>
<div className={css.perNight}>
<FormattedMessage id="ManageListingCard.perNight" />
<div className={css.perUnit}>
<FormattedMessage id="ManageListingCard.perUnit" />
</div>
</div>
<div className={css.mainInfo}>

View file

@ -110,7 +110,7 @@ exports[`ManageListingCard matches snapshot 1`] = `
</div>
<div>
<FormattedMessage
id="ManageListingCard.perNight"
id="ManageListingCard.perUnit"
values={Object {}}
/>
</div>

View file

@ -11,6 +11,8 @@ import {
PrimaryButton,
SecondaryButton,
} from '../../components';
import config from '../../config';
import css from './TransactionPanel.css';
// Functional component as a helper to build ActivityFeed section
@ -85,6 +87,7 @@ export const BreakdownMaybe = props => {
<BookingBreakdown
className={classes}
userRole={transactionRole}
unitType={config.bookingUnitType}
transaction={transaction}
booking={transaction.booking}
/>

View file

@ -14,6 +14,13 @@ const i18n = {
firstDayOfWeek: 0,
};
// The transaction line item code for the main unit type in bookings.
//
// Possible values: ['line-item/night', 'line-item/day']
//
// TODO: Check that this applies when API adds support for daily bookings
const bookingUnitType = 'line-item/night';
// To pass environment variables to the client app in the build
// script, react-scripts (and the sharetribe-scripts fork of
// react-scripts) require using the REACT_APP_ prefix to avoid
@ -262,6 +269,7 @@ const config = {
env,
dev,
locale,
bookingUnitType,
i18n,
sdk: { clientId: sdkClientId, baseUrl: sdkBaseUrl },
currency,

View file

@ -45,7 +45,8 @@ const estimatedNightlyTransaction = (bookingStart, bookingEnd, unitPrice) => {
payoutTotal: totalPrice,
lineItems: [
{
code: 'line-item/night',
code: propTypes.LINE_ITEM_NIGHT,
includeFor: ['customer', 'provider'],
unitPrice: unitPrice,
quantity: new Decimal(nightCount),
lineTotal: totalPrice,
@ -77,12 +78,14 @@ const estimatedBreakdown = (bookingStart, bookingEnd, unitPrice) => {
return null;
}
// TODO: estimate daily tx if config is set
const tx = estimatedNightlyTransaction(bookingStart, bookingEnd, unitPrice);
return (
<BookingBreakdown
className={css.receipt}
userRole="customer"
unitType={config.bookingUnitType}
transaction={tx}
booking={tx.booking}
/>

View file

@ -55,6 +55,7 @@ describe('BookingDatesForm', () => {
expect(transaction.attributes.lineItems).toEqual([
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
unitPrice: price,
quantity: new Decimal(2),
lineTotal: new Money(2198, 'USD'),

View file

@ -25,6 +25,7 @@ import {
import { StripePaymentForm } from '../../containers';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import { initiateOrder, setInitialValues, speculateTransaction } from './CheckoutPage.duck';
import config from '../../config';
import { storeData, storedData, clearData } from './CheckoutPageSessionHelpers';
import LogoIcon from './LogoIcon';
@ -203,6 +204,7 @@ export class CheckoutPageComponent extends Component {
<BookingBreakdown
className={css.bookingBreakdown}
userRole="customer"
unitType={config.bookingUnitType}
transaction={currentTransaction}
booking={currentBooking}
/>

View file

@ -25,7 +25,7 @@ export const EditListingPricingFormComponent = props => {
updateInProgress,
} = props;
const pricePerNightMessage = intl.formatMessage({ id: 'EditListingPricingForm.pricePerNight' });
const pricePerUnitMessage = intl.formatMessage({ id: 'EditListingPricingForm.pricePerUnit' });
const priceRequiredMessage = intl.formatMessage({ id: 'EditListingPricingForm.priceRequired' });
const pricePlaceholderMessage = intl.formatMessage({
id: 'EditListingPricingForm.priceInputPlaceholder',
@ -50,7 +50,7 @@ export const EditListingPricingFormComponent = props => {
className={css.priceInput}
autoFocus
name="price"
label={pricePerNightMessage}
label={pricePerUnitMessage}
placeholder={pricePlaceholderMessage}
currencyConfig={config.currencyConfig}
validate={[required(priceRequiredMessage)]}

View file

@ -13,7 +13,7 @@ exports[`EditListingPricingForm matches snapshot 1`] = `
<label
htmlFor="EditListingPricingForm.CurrencyInputField"
>
EditListingPricingForm.pricePerNight
EditListingPricingForm.pricePerUnit
</label>
<input
autoFocus={true}

View file

@ -98,6 +98,10 @@ exports[`InboxPage matches snapshot 1`] = `
"lineItems": Array [
Object {
"code": "line-item/night",
"includeFor": Array [
"customer",
"provider",
],
"lineTotal": Money {
"amount": 1000,
"currency": "USD",
@ -111,6 +115,9 @@ exports[`InboxPage matches snapshot 1`] = `
},
Object {
"code": "line-item/provider-commission",
"includeFor": Array [
"provider",
],
"lineTotal": Money {
"amount": -100,
"currency": "USD",
@ -213,6 +220,10 @@ exports[`InboxPage matches snapshot 1`] = `
"lineItems": Array [
Object {
"code": "line-item/night",
"includeFor": Array [
"customer",
"provider",
],
"lineTotal": Money {
"amount": 1000,
"currency": "USD",
@ -226,6 +237,9 @@ exports[`InboxPage matches snapshot 1`] = `
},
Object {
"code": "line-item/provider-commission",
"includeFor": Array [
"provider",
],
"lineTotal": Money {
"amount": -100,
"currency": "USD",
@ -486,6 +500,10 @@ exports[`InboxPage matches snapshot 3`] = `
"lineItems": Array [
Object {
"code": "line-item/night",
"includeFor": Array [
"customer",
"provider",
],
"lineTotal": Money {
"amount": 1000,
"currency": "USD",
@ -499,6 +517,9 @@ exports[`InboxPage matches snapshot 3`] = `
},
Object {
"code": "line-item/provider-commission",
"includeFor": Array [
"provider",
],
"lineTotal": Money {
"amount": -100,
"currency": "USD",
@ -601,6 +622,10 @@ exports[`InboxPage matches snapshot 3`] = `
"lineItems": Array [
Object {
"code": "line-item/night",
"includeFor": Array [
"customer",
"provider",
],
"lineTotal": Money {
"amount": 1000,
"currency": "USD",
@ -614,6 +639,9 @@ exports[`InboxPage matches snapshot 3`] = `
},
Object {
"code": "line-item/provider-commission",
"includeFor": Array [
"provider",
],
"lineTotal": Money {
"amount": -100,
"currency": "USD",

View file

@ -270,7 +270,7 @@
}
}
.desktopPerNight {
.desktopPerUnit {
/* Font */
@apply --marketplaceH5FontStyles;
color: var(--matterColor);
@ -565,7 +565,7 @@
}
}
.perNight {
.perUnit {
/* Font */
@apply --marketplaceTinyFontStyles;
color: var(--matterColor);

View file

@ -505,8 +505,8 @@ export class ListingPageComponent extends Component {
<div className={css.desktopPriceValue} title={priceTitle}>
{formattedPrice}
</div>
<div className={css.desktopPerNight}>
<FormattedMessage id="ListingPage.perNight" />
<div className={css.desktopPerUnit}>
<FormattedMessage id="ListingPage.perUnit" />
</div>
</div>
<div className={css.heading}>
@ -618,8 +618,8 @@ export class ListingPageComponent extends Component {
<div className={css.priceValue} title={priceTitle}>
{formattedPrice}
</div>
<div className={css.perNight}>
<FormattedMessage id="ListingPage.perNight" />
<div className={css.perUnit}>
<FormattedMessage id="ListingPage.perUnit" />
</div>
</div>

View file

@ -174,7 +174,7 @@ exports[`ListingPage matches snapshot 1`] = `
</div>
<div>
<FormattedMessage
id="ListingPage.perNight"
id="ListingPage.perUnit"
values={Object {}}
/>
</div>
@ -398,7 +398,7 @@ exports[`ListingPage matches snapshot 1`] = `
</div>
<div>
<FormattedMessage
id="ListingPage.perNight"
id="ListingPage.perUnit"
values={Object {}}
/>
</div>

View file

@ -68,6 +68,10 @@ exports[`TransactionPage - Order matches snapshot 1`] = `
"lineItems": Array [
Object {
"code": "line-item/night",
"includeFor": Array [
"customer",
"provider",
],
"lineTotal": Money {
"amount": 1000,
"currency": "USD",
@ -81,6 +85,9 @@ exports[`TransactionPage - Order matches snapshot 1`] = `
},
Object {
"code": "line-item/provider-commission",
"includeFor": Array [
"provider",
],
"lineTotal": Money {
"amount": -100,
"currency": "USD",
@ -259,6 +266,10 @@ exports[`TransactionPage - Sale matches snapshot 1`] = `
"lineItems": Array [
Object {
"code": "line-item/night",
"includeFor": Array [
"customer",
"provider",
],
"lineTotal": Money {
"amount": 1000,
"currency": "USD",
@ -272,6 +283,9 @@ exports[`TransactionPage - Sale matches snapshot 1`] = `
},
Object {
"code": "line-item/provider-commission",
"includeFor": Array [
"provider",
],
"lineTotal": Money {
"amount": -100,
"currency": "USD",

View file

@ -39,7 +39,9 @@
"Avatar.bannedUserDisplayName": "Banned user",
"BookingBreakdown.bookingPeriod": "{bookingStart} {bookingEnd}",
"BookingBreakdown.commission": "Saunatime fee",
"BookingBreakdown.dayCount": "{count, number} {count, plural, one {day} other {days}}",
"BookingBreakdown.nightCount": "{count, number} {count, plural, one {night} other {nights}}",
"BookingBreakdown.pricePerDay": "Price per day",
"BookingBreakdown.pricePerNight": "Price per night",
"BookingBreakdown.providerTotalCanceled": "Total price",
"BookingBreakdown.providerTotalDeclined": "You would have made",
@ -143,7 +145,7 @@
"EditListingPhotosPanel.payoutModalTitlePayoutPreferences": "Payout preferences",
"EditListingPhotosPanel.title": "Edit the photos of {listingTitle}",
"EditListingPricingForm.priceInputPlaceholder": "Choose your price…",
"EditListingPricingForm.pricePerNight": "Price per night in euros",
"EditListingPricingForm.pricePerUnit": "Price per night in euros",
"EditListingPricingForm.priceRequired": "You need to add a valid price.",
"EditListingPricingForm.updateFailed": "Failed to update listing. Please try again.",
"EditListingPricingPanel.createListingTitle": "How much does it cost?",
@ -234,7 +236,7 @@
"LandingPage.schemaDescription": "Book a sauna using Saunatime or earn some income by sharing your sauna",
"LandingPage.schemaTitle": "Book saunas everywhere | {siteTitle}",
"ListingCard.hostedBy": "Hosted by {authorName}.",
"ListingCard.perNight": "per night",
"ListingCard.perUnit": "per night",
"ListingCard.unsupportedPrice": "({currency})",
"ListingCard.unsupportedPriceTitle": "Unsupported currency ({currency})",
"ListingPage.bannedUserDisplayName": "Banned user",
@ -260,7 +262,7 @@
"ListingPage.locationTitle": "Location",
"ListingPage.ownClosedListing": "Your listing has been closed and can't be booked.",
"ListingPage.ownListing": "This is your own listing.",
"ListingPage.perNight": "per night",
"ListingPage.perUnit": "per night",
"ListingPage.reviewsError": "Loading reviews failed.",
"ListingPage.reviewsHeading": "Reviews ({count})",
"ListingPage.schemaTitle": "{title} - {price} | {siteTitle}",
@ -282,7 +284,7 @@
"ManageListingCard.closedListing": "This listing is closed and not visible on the marketplace.",
"ManageListingCard.edit": "Edit",
"ManageListingCard.openListing": "Open listing",
"ManageListingCard.perNight": "per night",
"ManageListingCard.perUnit": "per night",
"ManageListingCard.unsupportedPrice": "({currency})",
"ManageListingCard.unsupportedPriceTitle": "Unsupported currency ({currency})",
"ManageListingCard.viewListing": "View listing",

View file

@ -23,6 +23,26 @@ export const nightsBetween = (startDate, endDate) => {
return nights;
};
/**
* Calculate the number of days between the given dates
*
* @param {Date} startDate start of the time period
* @param {Date} endDate end of the time period. NOTE: with daily
* bookings, it is expected that this date is the exclusive end date,
* i.e. the last day of the booking is the previous date of this end
* date.
*
* @throws Will throw if the end date is before the start date
* @returns {Number} number of days between the given dates
*/
export const daysBetween = (startDate, endDate) => {
const days = moment(endDate).diff(startDate, 'days');
if (days < 0) {
throw new Error('End date cannot be before start date');
}
return days;
};
/**
* Format the given date
*

View file

@ -1,5 +1,5 @@
import { fakeIntl } from './test-data';
import { nightsBetween, formatDate } from './dates';
import { nightsBetween, daysBetween, formatDate } from './dates';
describe('date utils', () => {
describe('nightsBetween()', () => {
@ -24,6 +24,28 @@ describe('date utils', () => {
});
});
describe('daysBetween()', () => {
it('should fail if end date is before start date', () => {
const start = new Date(2017, 0, 2);
const end = new Date(2017, 0, 1);
expect(() => daysBetween(start, end)).toThrow('End date cannot be before start date');
});
it('should handle equal start and end dates', () => {
const d = new Date(2017, 0, 1);
expect(daysBetween(d, d)).toEqual(0);
});
it('should calculate night count for a single day', () => {
const start = new Date(2017, 0, 1);
const end = new Date(2017, 0, 2);
expect(daysBetween(start, end)).toEqual(1);
});
it('should calculate day count', () => {
const start = new Date(2017, 0, 1);
const end = new Date(2017, 0, 3);
expect(daysBetween(start, end)).toEqual(2);
});
});
describe('formatDate()', () => {
/*
NOTE: These are not really testing the formatting properly since

View file

@ -300,6 +300,15 @@ export const review = shape({
subject: user,
});
export const LINE_ITEM_NIGHT = 'line-item/night';
// TODO: This is speculative, check again when the API adds the day support
export const LINE_ITEM_DAY = 'line-item/day';
export const LINE_ITEM_PROVIDER_COMMISSION = 'line-item/provider-commission';
const LINE_ITEMS = [LINE_ITEM_NIGHT, LINE_ITEM_DAY, LINE_ITEM_PROVIDER_COMMISSION];
// Denormalised transaction object
export const transaction = shape({
id: uuid.isRequired,
@ -316,7 +325,8 @@ export const transaction = shape({
lineItems: arrayOf(
shape({
code: string.isRequired,
code: oneOf(LINE_ITEMS).isRequired,
includeFor: arrayOf(oneOf(['customer', 'provider'])).isRequired,
quantity: instanceOf(Decimal),
unitPrice: money.isRequired,
lineTotal: money.isRequired,

View file

@ -134,6 +134,7 @@ export const createTransaction = options => {
lineItems: [
{
code: 'line-item/night',
includeFor: ['customer', 'provider'],
quantity: new Decimal(nightCount),
unitPrice: new Money(total.amount / nightCount, total.currency),
lineTotal: total,
@ -141,6 +142,7 @@ export const createTransaction = options => {
},
{
code: 'line-item/provider-commission',
includeFor: ['provider'],
unitPrice: new Money(commission.amount * -1, commission.currency),
lineTotal: new Money(commission.amount * -1, commission.currency),
reversal: false,