Merge pull request #868 from sharetribe/fetch-availability-data

Fetch availability data
This commit is contained in:
Hannu Lyytikäinen 2018-07-23 18:43:58 +03:00 committed by GitHub
commit 0d14e447bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 96 additions and 12 deletions

View file

@ -48,7 +48,7 @@
"redux-thunk": "^2.2.0",
"sanitize.css": "^5.0.0",
"sharetribe-scripts": "1.1.2",
"sharetribe-sdk": "https://github.com/sharetribe/sharetribe-sdk-js.git#b438be11d0f40f62e1a0388da745f6792420ddb1",
"sharetribe-sdk": "https://github.com/sharetribe/sharetribe-sdk-js.git#b7ffe0fe0a5bdb0a1471e2ff36cdd25b997fb39b",
"smoothscroll-polyfill": "^0.4.0",
"source-map-support": "^0.5.4",
"url": "^0.11.0"

View file

@ -75,11 +75,9 @@ const defaultProps = {
enableOutsideDays: false,
isDayBlocked: () => false,
// Stripe holds funds in a reserve for up to 90 days from charge creation.
// outside range -><- today ... today+89 days -><- outside range
// outside range -><- today ... today+available days -1 -><- outside range
isOutsideRange: day => {
const daysCountAvailableToBook = 90;
const endOfRange = daysCountAvailableToBook - 1;
const endOfRange = config.dayCountAvailableForBooking - 1;
return (
!isInclusivelyAfterDay(day, moment()) ||
!isInclusivelyBeforeDay(day, moment().add(endOfRange, 'days'))

View file

@ -117,11 +117,9 @@ const defaultProps = {
enableOutsideDays: false,
isDayBlocked: () => false,
// Stripe holds funds in a reserve for up to 90 days from charge creation.
// outside range -><- today ... today+89 days -><- outside range
// outside range -><- today ... today+available days -1 -><- outside range
isOutsideRange: day => {
const daysCountAvailableToBook = 90;
const endOfRange = daysCountAvailableToBook - 1;
const endOfRange = config.dayCountAvailableForBooking - 1;
return (
!isInclusivelyAfterDay(day, moment()) ||
!isInclusivelyBeforeDay(day, moment().add(endOfRange, 'days'))

View file

@ -36,6 +36,15 @@ const bookingProcessAlias = 'preauth-with-nightly-booking/release-1';
// translations when the unit is changed.
const bookingUnitType = 'line-item/night';
// Should the application fetch available time slots (currently defined as
// start and end dates) to be shown on listing page.
const fetchAvailableTimeSlots = false;
// A maximum number of days forwards during which a booking can be made.
// This is limited due to Stripe holding funds up to 90 days from the
// moment they are charged.
const dayCountAvailableForBooking = 90;
// 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
@ -313,6 +322,8 @@ const config = {
locale,
bookingProcessAlias,
bookingUnitType,
fetchAvailableTimeSlots,
dayCountAvailableForBooking,
i18n,
sdk: {
clientId: sdkClientId,

View file

@ -1,4 +1,5 @@
import pick from 'lodash/pick';
import moment from 'moment';
import config from '../../config';
import { types as sdkTypes } from '../../util/sdkLoader';
import { storableError } from '../../util/errors';
@ -21,6 +22,10 @@ export const FETCH_REVIEWS_REQUEST = 'app/ListingPage/FETCH_REVIEWS_REQUEST';
export const FETCH_REVIEWS_SUCCESS = 'app/ListingPage/FETCH_REVIEWS_SUCCESS';
export const FETCH_REVIEWS_ERROR = 'app/ListingPage/FETCH_REVIEWS_ERROR';
export const FETCH_TIME_SLOTS_REQUEST = 'app/ListingPage/FETCH_TIME_SLOTS_REQUEST';
export const FETCH_TIME_SLOTS_SUCCESS = 'app/ListingPage/FETCH_TIME_SLOTS_SUCCESS';
export const FETCH_TIME_SLOTS_ERROR = 'app/ListingPage/FETCH_TIME_SLOTS_ERROR';
export const SEND_ENQUIRY_REQUEST = 'app/ListingPage/SEND_ENQUIRY_REQUEST';
export const SEND_ENQUIRY_SUCCESS = 'app/ListingPage/SEND_ENQUIRY_SUCCESS';
export const SEND_ENQUIRY_ERROR = 'app/ListingPage/SEND_ENQUIRY_ERROR';
@ -32,6 +37,8 @@ const initialState = {
showListingError: null,
reviews: [],
fetchReviewsError: null,
timeSlots: [],
fetchTimesLotsError: null,
sendEnquiryInProgress: false,
sendEnquiryError: null,
enquiryModalOpenForListingId: null,
@ -55,6 +62,13 @@ const listingPageReducer = (state = initialState, action = {}) => {
case FETCH_REVIEWS_ERROR:
return { ...state, fetchReviewsError: payload };
case FETCH_TIME_SLOTS_REQUEST:
return { ...state, fetchTimeSlotsError: null };
case FETCH_TIME_SLOTS_SUCCESS:
return { ...state, timeSlots: payload };
case FETCH_TIME_SLOTS_ERROR:
return { ...state, fetchTimeSlotsError: payload };
case SEND_ENQUIRY_REQUEST:
return { ...state, sendEnquiryInProgress: true, sendEnquiryError: null };
case SEND_ENQUIRY_SUCCESS:
@ -95,6 +109,17 @@ export const fetchReviewsError = error => ({
payload: error,
});
export const fetchTimeSlotsRequest = () => ({ type: FETCH_TIME_SLOTS_REQUEST });
export const fetchTimeSlotsSuccess = timeSlots => ({
type: FETCH_TIME_SLOTS_SUCCESS,
payload: timeSlots,
});
export const fetchTimeSlotsError = error => ({
type: FETCH_TIME_SLOTS_ERROR,
error: true,
payload: error,
});
export const sendEnquiryRequest = () => ({ type: SEND_ENQUIRY_REQUEST });
export const sendEnquirySuccess = () => ({ type: SEND_ENQUIRY_SUCCESS });
export const sendEnquiryError = e => ({ type: SEND_ENQUIRY_ERROR, error: true, payload: e });
@ -143,6 +168,7 @@ export const showListing = (listingId, isOwn = false) => (dispatch, getState, sd
};
export const fetchReviews = listingId => (dispatch, getState, sdk) => {
dispatch(fetchReviewsRequest);
return sdk.reviews
.query({
listing_id: listingId,
@ -159,6 +185,19 @@ export const fetchReviews = listingId => (dispatch, getState, sdk) => {
});
};
export const fetchTimeSlots = params => (dispatch, getState, sdk) => {
dispatch(fetchTimeSlotsRequest);
return sdk.timeslots
.query(params)
.then(response => {
const timeSlots = denormalisedResponseEntities(response);
dispatch(fetchTimeSlotsSuccess(timeSlots));
})
.catch(e => {
dispatch(fetchTimeSlotsError(storableError(e)));
});
};
export const sendEnquiry = (listingId, message) => (dispatch, getState, sdk) => {
dispatch(sendEnquiryRequest());
const bodyParams = {
@ -190,5 +229,23 @@ export const loadData = (params, search) => dispatch => {
if (params.variant === LISTING_PAGE_PENDING_APPROVAL_VARIANT) {
return dispatch(showListing(listingId, true));
}
return Promise.all([dispatch(showListing(listingId)), dispatch(fetchReviews(listingId))]);
if (config.fetchAvailableTimeSlots) {
// fetch time slots for 90 days starting today
// as the booking can only be done for 90 days
// in the future due to Stripe limitations
const start = moment().toDate();
const end = moment()
.add(config.dayCountAvailableForBooking, 'days')
.toDate();
const timeSlotsParams = { listingId, start, end };
return Promise.all([
dispatch(showListing(listingId)),
dispatch(fetchTimeSlots(timeSlotsParams)),
dispatch(fetchReviews(listingId)),
]);
} else {
return Promise.all([dispatch(showListing(listingId)), dispatch(fetchReviews(listingId))]);
}
};

View file

@ -497,6 +497,8 @@ ListingPageComponent.defaultProps = {
showListingError: null,
reviews: [],
fetchReviewsError: null,
timeSlots: [],
fetchTimeSlotsError: null,
sendEnquiryError: null,
categoriesConfig: config.custom.categories,
amenitiesConfig: config.custom.amenities,
@ -532,6 +534,8 @@ ListingPageComponent.propTypes = {
useInitialValues: func.isRequired,
reviews: arrayOf(propTypes.review),
fetchReviewsError: propTypes.error,
timeSlots: arrayOf(propTypes.timeSlot),
fetchTimeSlotsError: propTypes.error,
sendEnquiryInProgress: bool.isRequired,
sendEnquiryError: propTypes.error,
onSendEnquiry: func.isRequired,
@ -546,6 +550,8 @@ const mapStateToProps = state => {
showListingError,
reviews,
fetchReviewsError,
timeSlots,
fetchTimeSlotsError,
sendEnquiryInProgress,
sendEnquiryError,
enquiryModalOpenForListingId,
@ -574,6 +580,8 @@ const mapStateToProps = state => {
showListingError,
reviews,
fetchReviewsError,
timeSlots,
fetchTimeSlotsError,
sendEnquiryInProgress,
sendEnquiryError,
};

View file

@ -187,6 +187,16 @@ propTypes.booking = shape({
}),
});
// Denormalised time slot object
propTypes.timeSlot = shape({
id: propTypes.uuid.isRequired,
type: propTypes.value('timeSlot').isRequired,
attributes: shape({
end: instanceOf(Date).isRequired,
start: instanceOf(Date).isRequired,
}),
});
// When a customer makes a booking to a listing, a transaction is
// created with the initial request transition.
export const TRANSITION_REQUEST = 'transition/request';
@ -425,6 +435,7 @@ export const ERROR_CODE_EMAIL_NOT_VERIFIED = 'email-unverified';
export const ERROR_CODE_TOO_MANY_VERIFICATION_REQUESTS = 'email-too-many-verification-requests';
export const ERROR_CODE_UPLOAD_OVER_LIMIT = 'request-upload-over-limit';
export const ERROR_CODE_VALIDATION_INVALID_PARAMS = 'validation-invalid-params';
export const ERROR_CODE_VALIDATION_INVALID_VALUE = 'validation-invalid-value';
export const ERROR_CODE_NOT_FOUND = 'not-found';
export const ERROR_CODE_FORBIDDEN = 'forbidden';
export const ERROR_CODE_MISSING_STRIPE_ACCOUNT = 'transaction-missing-stripe-account';
@ -441,6 +452,7 @@ const ERROR_CODES = [
ERROR_CODE_TOO_MANY_VERIFICATION_REQUESTS,
ERROR_CODE_UPLOAD_OVER_LIMIT,
ERROR_CODE_VALIDATION_INVALID_PARAMS,
ERROR_CODE_VALIDATION_INVALID_VALUE,
ERROR_CODE_NOT_FOUND,
ERROR_CODE_FORBIDDEN,
ERROR_CODE_MISSING_STRIPE_ACCOUNT,

View file

@ -7574,9 +7574,9 @@ sharetribe-scripts@1.1.2:
optionalDependencies:
fsevents "^1.1.3"
"sharetribe-sdk@https://github.com/sharetribe/sharetribe-sdk-js.git#b438be11d0f40f62e1a0388da745f6792420ddb1":
"sharetribe-sdk@https://github.com/sharetribe/sharetribe-sdk-js.git#b7ffe0fe0a5bdb0a1471e2ff36cdd25b997fb39b":
version "0.0.1"
resolved "https://github.com/sharetribe/sharetribe-sdk-js.git#b438be11d0f40f62e1a0388da745f6792420ddb1"
resolved "https://github.com/sharetribe/sharetribe-sdk-js.git#b7ffe0fe0a5bdb0a1471e2ff36cdd25b997fb39b"
dependencies:
axios "^0.15.3"
js-cookie "^2.1.3"