Merge pull request #789 from sharetribe/listing-minimum-price

Add support for configuring a minimum price for a listing
This commit is contained in:
Kimmo Puputti 2018-04-03 10:41:15 +03:00 committed by GitHub
commit 228a4d716d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 68 additions and 6 deletions

View file

@ -37,6 +37,10 @@ const sdkBaseUrl = process.env.REACT_APP_SHARETRIBE_SDK_BASE_URL;
const currency = process.env.REACT_APP_SHARETRIBE_MARKETPLACE_CURRENCY;
// Listing minimum price in currency sub units, e.g. cents.
// 0 means no restriction to the price
const listingMinimumPriceSubUnits = 0;
// Sentry DSN (Data Source Name), a client key for authenticating calls to Sentry
const sentryDsn = process.env.REACT_APP_PUBLIC_SENTRY_DSN;
@ -283,6 +287,7 @@ const config = {
sdk: { clientId: sdkClientId, baseUrl: sdkBaseUrl },
sortSearchByDistance,
currency,
listingMinimumPriceSubUnits,
currencyConfig,
stripe: { publishableKey: stripePublishableKey, supportedCountries: stripeSupportedCountries },
canonicalRootURL,

View file

@ -6,11 +6,15 @@ import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import config from '../../config';
import { propTypes } from '../../util/types';
import { required } from '../../util/validators';
import * as validators from '../../util/validators';
import { formatMoney } from '../../util/currency';
import { types as sdkTypes } from '../../util/sdkLoader';
import { Form, Button, FieldCurrencyInput } from '../../components';
import css from './EditListingPricingForm.css';
const { Money } = sdkTypes;
export const EditListingPricingFormComponent = props => {
const {
className,
@ -26,11 +30,31 @@ export const EditListingPricingFormComponent = props => {
} = props;
const pricePerUnitMessage = intl.formatMessage({ id: 'EditListingPricingForm.pricePerUnit' });
const priceRequiredMessage = intl.formatMessage({ id: 'EditListingPricingForm.priceRequired' });
const pricePlaceholderMessage = intl.formatMessage({
id: 'EditListingPricingForm.priceInputPlaceholder',
});
const priceRequired = validators.required(
intl.formatMessage({
id: 'EditListingPricingForm.priceRequired',
})
);
const minPrice = new Money(config.listingMinimumPriceSubUnits, config.currency);
const minPriceRequired = validators.moneySubUnitAmountAtLeast(
intl.formatMessage(
{
id: 'EditListingPricingForm.priceTooLow',
},
{
minPrice: formatMoney(intl, minPrice),
}
),
config.listingMinimumPriceSubUnits
);
const priceValidators = config.listingMinimumPriceSubUnits
? [priceRequired, minPriceRequired]
: [priceRequired];
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPricingForm.updateFailed" />
@ -53,7 +77,7 @@ export const EditListingPricingFormComponent = props => {
label={pricePerUnitMessage}
placeholder={pricePlaceholderMessage}
currencyConfig={config.currencyConfig}
validate={[required(priceRequiredMessage)]}
validate={priceValidators}
/>
<Button

View file

@ -163,6 +163,7 @@
"EditListingPricingForm.priceInputPlaceholder": "Choose your price…",
"EditListingPricingForm.pricePerUnit": "Price per night in euros",
"EditListingPricingForm.priceRequired": "You need to add a valid price.",
"EditListingPricingForm.priceTooLow": "Price should be at least {minPrice}.",
"EditListingPricingForm.updateFailed": "Failed to update listing. Please try again.",
"EditListingPricingPanel.createListingTitle": "How much does it cost?",
"EditListingPricingPanel.listingPriceCurrencyInvalid": "Listing currency is different from the marketplace currency. You cannot edit the price.",

View file

@ -1,8 +1,8 @@
import moment from 'moment';
import { types } from 'sharetribe-sdk';
import { types as sdkTypes } from './sdkLoader';
import { toPairs } from 'lodash';
const { LatLng } = types;
const { LatLng, Money } = sdkTypes;
export const PASSWORD_MIN_LENGTH = 8;
export const PASSWORD_MAX_LENGTH = 256;
@ -110,6 +110,10 @@ export const emailFormatValid = message => value => {
return value && EMAIL_RE.test(value) ? VALID : message;
};
export const moneySubUnitAmountAtLeast = (message, minValue) => value => {
return value instanceof Money && value.amount >= minValue ? VALID : message;
};
export const ageAtLeast = (message, minYears) => value => {
const now = moment();
const ageInYears = now.diff(moment(value), 'years', true);

View file

@ -1,4 +1,13 @@
import { required, requiredStringNoTrim, minLength, maxLength } from './validators';
import { types as sdkTypes } from './sdkLoader';
import {
required,
requiredStringNoTrim,
minLength,
maxLength,
moneySubUnitAmountAtLeast,
} from './validators';
const { Money } = sdkTypes;
describe('validators', () => {
describe('required()', () => {
@ -120,4 +129,23 @@ describe('validators', () => {
expect(maxLength('fail', 3)([1, 2, 3, 4])).toEqual('fail');
});
});
describe('moneySubUnitAmountAtLeast()', () => {
it('should not allow empty or missing value', () => {
expect(moneySubUnitAmountAtLeast('fail', 50)(undefined)).toEqual('fail');
expect(moneySubUnitAmountAtLeast('fail', 50)(null)).toEqual('fail');
expect(moneySubUnitAmountAtLeast('fail', 50)('')).toEqual('fail');
expect(moneySubUnitAmountAtLeast('fail', 50)(0)).toEqual('fail');
expect(moneySubUnitAmountAtLeast('fail', 50)(50)).toEqual('fail');
expect(moneySubUnitAmountAtLeast('fail', 50)(100)).toEqual('fail');
});
it('should not allow too low values', () => {
expect(moneySubUnitAmountAtLeast('fail', 50)(new Money(0, 'USD'))).toEqual('fail');
expect(moneySubUnitAmountAtLeast('fail', 50)(new Money(49, 'USD'))).toEqual('fail');
});
it('should allow large enough values', () => {
expect(moneySubUnitAmountAtLeast('fail', 0)(new Money(0, 'USD'))).toBeUndefined();
expect(moneySubUnitAmountAtLeast('fail', 50)(new Money(50, 'USD'))).toBeUndefined();
expect(moneySubUnitAmountAtLeast('fail', 50)(new Money(100, 'USD'))).toBeUndefined();
});
});
});