mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
423 lines
13 KiB
JavaScript
423 lines
13 KiB
JavaScript
/**
|
|
* This module defines custom PropTypes shared within the application.
|
|
*
|
|
* To learn about validating React component props with PropTypes, see:
|
|
*
|
|
* https://facebook.github.io/react/docs/typechecking-with-proptypes.html
|
|
*
|
|
* For component specific PropTypes, it's perfectly ok to inline them
|
|
* to the component itself. If the type is shared or external (SDK or
|
|
* API), however, it should be in this file for sharing with other
|
|
* components.
|
|
*
|
|
* PropTypes should usually be validated only at the lowest level
|
|
* where the props are used, not along the way in parents that pass
|
|
* along the props to their children. Those parents should usually
|
|
* just validate the presense of the prop key and that the value is
|
|
* defined. This way we get the validation errors only in the most
|
|
* specific place and avoid duplicate errros.
|
|
*/
|
|
import {
|
|
arrayOf,
|
|
bool,
|
|
func,
|
|
instanceOf,
|
|
number,
|
|
object,
|
|
oneOf,
|
|
oneOfType,
|
|
shape,
|
|
string,
|
|
} from 'prop-types';
|
|
import Decimal from 'decimal.js';
|
|
import { types as sdkTypes } from './sdkLoader';
|
|
import { ensureTransaction } from './data';
|
|
|
|
const { UUID, LatLng, LatLngBounds, Money } = sdkTypes;
|
|
|
|
const propTypes = {};
|
|
|
|
// Fixed value
|
|
propTypes.value = val => oneOf([val]);
|
|
|
|
// SDK type instances
|
|
propTypes.uuid = instanceOf(UUID);
|
|
propTypes.latlng = instanceOf(LatLng);
|
|
propTypes.latlngBounds = instanceOf(LatLngBounds);
|
|
propTypes.money = instanceOf(Money);
|
|
|
|
// Configuration for currency formatting
|
|
propTypes.currencyConfig = shape({
|
|
style: string.isRequired,
|
|
currency: string.isRequired,
|
|
currencyDisplay: string,
|
|
useGrouping: bool,
|
|
minimumFractionDigits: number,
|
|
maximumFractionDigits: number,
|
|
});
|
|
|
|
// Configuration for a single route
|
|
propTypes.route = shape({
|
|
name: string.isRequired,
|
|
path: string.isRequired,
|
|
exact: bool,
|
|
strict: bool,
|
|
component: func.isRequired,
|
|
loadData: func,
|
|
});
|
|
|
|
// Place object from LocationAutocompleteInput
|
|
propTypes.place = shape({
|
|
address: string.isRequired,
|
|
origin: propTypes.latlng.isRequired,
|
|
bounds: propTypes.latlngBounds, // optional viewport bounds
|
|
country: string, // country code, e.g. FI, US
|
|
});
|
|
|
|
// Denormalised image object
|
|
propTypes.image = shape({
|
|
id: propTypes.uuid.isRequired,
|
|
type: propTypes.value('image').isRequired,
|
|
attributes: shape({
|
|
sizes: arrayOf(
|
|
shape({
|
|
width: number.isRequired,
|
|
height: number.isRequired,
|
|
name: string.isRequired,
|
|
url: string.isRequired,
|
|
})
|
|
).isRequired,
|
|
}),
|
|
});
|
|
|
|
// Denormalised user object
|
|
propTypes.currentUser = shape({
|
|
id: propTypes.uuid.isRequired,
|
|
type: propTypes.value('currentUser').isRequired,
|
|
attributes: shape({
|
|
banned: bool.isRequired,
|
|
email: string.isRequired,
|
|
emailVerified: bool.isRequired,
|
|
profile: shape({
|
|
firstName: string.isRequired,
|
|
lastName: string.isRequired,
|
|
displayName: string.isRequired,
|
|
abbreviatedName: string.isRequired,
|
|
bio: string,
|
|
}).isRequired,
|
|
stripeConnected: bool.isRequired,
|
|
}),
|
|
profileImage: propTypes.image,
|
|
});
|
|
|
|
// Denormalised user object
|
|
propTypes.user = shape({
|
|
id: propTypes.uuid.isRequired,
|
|
type: propTypes.value('user').isRequired,
|
|
attributes: shape({
|
|
banned: bool.isRequired,
|
|
profile: shape({
|
|
displayName: string.isRequired,
|
|
abbreviatedName: string.isRequired,
|
|
bio: string,
|
|
}),
|
|
}),
|
|
profileImage: propTypes.image,
|
|
});
|
|
|
|
const listingAttributes = shape({
|
|
title: string.isRequired,
|
|
description: string.isRequired,
|
|
address: string.isRequired,
|
|
geolocation: propTypes.latlng.isRequired,
|
|
closed: bool.isRequired,
|
|
deleted: propTypes.value(false).isRequired,
|
|
price: propTypes.money,
|
|
customAttributes: object,
|
|
publicData: object.isRequired,
|
|
});
|
|
|
|
const ownListingAttributes = shape({
|
|
title: string.isRequired,
|
|
description: string.isRequired,
|
|
address: string.isRequired,
|
|
geolocation: propTypes.latlng.isRequired,
|
|
closed: bool.isRequired,
|
|
deleted: propTypes.value(false).isRequired,
|
|
price: propTypes.money,
|
|
customAttributes: object,
|
|
publicData: object.isRequired,
|
|
});
|
|
|
|
const deletedListingAttributes = shape({
|
|
closed: bool.isRequired,
|
|
deleted: propTypes.value(true).isRequired,
|
|
});
|
|
|
|
// Denormalised listing object
|
|
propTypes.listing = shape({
|
|
id: propTypes.uuid.isRequired,
|
|
type: propTypes.value('listing').isRequired,
|
|
attributes: oneOfType([listingAttributes, deletedListingAttributes]).isRequired,
|
|
author: propTypes.user,
|
|
images: arrayOf(propTypes.image),
|
|
});
|
|
|
|
// Denormalised ownListing object
|
|
propTypes.ownListing = shape({
|
|
id: propTypes.uuid.isRequired,
|
|
type: propTypes.value('ownListing').isRequired,
|
|
attributes: oneOfType([ownListingAttributes, deletedListingAttributes]).isRequired,
|
|
author: propTypes.user,
|
|
images: arrayOf(propTypes.image),
|
|
});
|
|
|
|
// Denormalised booking object
|
|
propTypes.booking = shape({
|
|
id: propTypes.uuid.isRequired,
|
|
type: propTypes.value('booking').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';
|
|
|
|
// A customer can also initiate a transaction with an enquiry, and
|
|
// then transition that with a request.
|
|
export const TRANSITION_ENQUIRE = 'transition/enquire';
|
|
export const TRANSITION_REQUEST_AFTER_ENQUIRY = 'transition/request-after-enquiry';
|
|
|
|
// When the provider accepts or declines a transaction from the
|
|
// SalePage, it is transitioned with the accept or decline transition.
|
|
export const TRANSITION_ACCEPT = 'transition/accept';
|
|
export const TRANSITION_DECLINE = 'transition/decline';
|
|
|
|
// The backend automatically expire the transaction.
|
|
export const TRANSITION_EXPIRE = 'transition/expire';
|
|
|
|
// Admin can also cancel the transition.
|
|
export const TRANSITION_CANCEL = 'transition/cancel';
|
|
|
|
// The backend will mark the transaction completed.
|
|
export const TRANSITION_COMPLETE = 'transition/complete';
|
|
|
|
// Reviews are given through transaction transitions. Review 1 can be
|
|
// by provider or customer, and review 2 will be the other party of
|
|
// the transaction.
|
|
export const TRANSITION_REVIEW_1_BY_PROVIDER = 'transition/review-1_by-provider';
|
|
export const TRANSITION_REVIEW_2_BY_PROVIDER = 'transition/review-2-by-provider';
|
|
export const TRANSITION_REVIEW_1_BY_CUSTOMER = 'transition/review-1-by-customer';
|
|
export const TRANSITION_REVIEW_2_BY_CUSTOMER = 'transition/review-2-by-customer';
|
|
export const TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD = 'transition/expire-customer-review-period';
|
|
export const TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD = 'transition/expire-provider-review-period';
|
|
export const TRANSITION_EXPIRE_REVIEW_PERIOD = 'transition/expire-review-period';
|
|
|
|
export const TRANSITIONS = [
|
|
TRANSITION_ACCEPT,
|
|
TRANSITION_CANCEL,
|
|
TRANSITION_COMPLETE,
|
|
TRANSITION_DECLINE,
|
|
TRANSITION_ENQUIRE,
|
|
TRANSITION_EXPIRE,
|
|
TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD,
|
|
TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD,
|
|
TRANSITION_EXPIRE_REVIEW_PERIOD,
|
|
TRANSITION_REQUEST,
|
|
TRANSITION_REQUEST_AFTER_ENQUIRY,
|
|
TRANSITION_REVIEW_1_BY_CUSTOMER,
|
|
TRANSITION_REVIEW_1_BY_PROVIDER,
|
|
TRANSITION_REVIEW_2_BY_CUSTOMER,
|
|
TRANSITION_REVIEW_2_BY_PROVIDER,
|
|
];
|
|
|
|
// Roles of actors that perform transaction transitions
|
|
export const TX_TRANSITION_ACTOR_CUSTOMER = 'customer';
|
|
export const TX_TRANSITION_ACTOR_PROVIDER = 'provider';
|
|
export const TX_TRANSITION_ACTOR_SYSTEM = 'system';
|
|
export const TX_TRANSITION_ACTOR_OPERATOR = 'operator';
|
|
|
|
export const TX_TRANSITION_ACTORS = [
|
|
TX_TRANSITION_ACTOR_CUSTOMER,
|
|
TX_TRANSITION_ACTOR_PROVIDER,
|
|
TX_TRANSITION_ACTOR_SYSTEM,
|
|
TX_TRANSITION_ACTOR_OPERATOR,
|
|
];
|
|
|
|
const txLastTransition = tx => ensureTransaction(tx).attributes.lastTransition;
|
|
|
|
export const txIsEnquired = tx => txLastTransition(tx) === TRANSITION_ENQUIRE;
|
|
|
|
export const txIsRequested = tx => {
|
|
const transition = txLastTransition(tx);
|
|
return transition === TRANSITION_REQUEST || transition === TRANSITION_REQUEST_AFTER_ENQUIRY;
|
|
};
|
|
|
|
export const txIsAccepted = tx => txLastTransition(tx) === TRANSITION_ACCEPT;
|
|
|
|
export const txIsDeclined = tx => txLastTransition(tx) === TRANSITION_DECLINE;
|
|
|
|
export const txIsExpired = tx => txLastTransition(tx) === TRANSITION_EXPIRE;
|
|
|
|
export const txIsDeclinedOrExpired = tx => txIsDeclined(tx) || txIsExpired(tx);
|
|
|
|
export const txIsCanceled = tx => txLastTransition(tx) === TRANSITION_CANCEL;
|
|
|
|
export const txIsCompleted = tx => txLastTransition(tx) === TRANSITION_COMPLETE;
|
|
|
|
export const txHasFirstReview = tx => firstReviewTransitions.includes(txLastTransition(tx));
|
|
|
|
export const txIsReviewed = tx => areReviewsCompleted(txLastTransition(tx));
|
|
|
|
propTypes.transition = shape({
|
|
at: instanceOf(Date).isRequired,
|
|
by: oneOf(TX_TRANSITION_ACTORS).isRequired,
|
|
transition: oneOf(TRANSITIONS).isRequired,
|
|
});
|
|
|
|
const firstReviewTransitions = [TRANSITION_REVIEW_1_BY_CUSTOMER, TRANSITION_REVIEW_1_BY_PROVIDER];
|
|
|
|
// Check if tx transition is followed by a state where
|
|
// reviews are completed
|
|
export const areReviewsCompleted = transition => {
|
|
return [
|
|
TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD,
|
|
TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD,
|
|
TRANSITION_EXPIRE_REVIEW_PERIOD,
|
|
TRANSITION_REVIEW_2_BY_CUSTOMER,
|
|
TRANSITION_REVIEW_2_BY_PROVIDER,
|
|
].includes(transition);
|
|
};
|
|
|
|
// Possible amount of stars in a review
|
|
export const REVIEW_RATINGS = [1, 2, 3, 4, 5];
|
|
|
|
// Review types: review of a provider and of a customer
|
|
export const REVIEW_TYPE_OF_PROVIDER = 'ofProvider';
|
|
export const REVIEW_TYPE_OF_CUSTOMER = 'ofCustomer';
|
|
|
|
// A review on a user
|
|
propTypes.review = shape({
|
|
id: propTypes.uuid.isRequired,
|
|
attributes: shape({
|
|
at: instanceOf(Date).isRequired,
|
|
content: string,
|
|
rating: oneOf(REVIEW_RATINGS),
|
|
state: string.isRequired,
|
|
type: oneOf([REVIEW_TYPE_OF_PROVIDER, REVIEW_TYPE_OF_CUSTOMER]).isRequired,
|
|
}).isRequired,
|
|
author: propTypes.user,
|
|
subject: propTypes.user,
|
|
});
|
|
|
|
export const LINE_ITEM_NIGHT = 'line-item/night';
|
|
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];
|
|
|
|
propTypes.bookingUnitType = oneOf([LINE_ITEM_NIGHT, LINE_ITEM_DAY]);
|
|
|
|
// Denormalised transaction object
|
|
propTypes.transaction = shape({
|
|
id: propTypes.uuid.isRequired,
|
|
type: propTypes.value('transaction').isRequired,
|
|
attributes: shape({
|
|
createdAt: instanceOf(Date).isRequired,
|
|
lastTransitionedAt: instanceOf(Date).isRequired,
|
|
lastTransition: oneOf(TRANSITIONS).isRequired,
|
|
|
|
// An enquiry won't need a total sum nor a booking so these are
|
|
// optional.
|
|
payinTotal: propTypes.money,
|
|
payoutTotal: propTypes.money,
|
|
|
|
lineItems: arrayOf(
|
|
shape({
|
|
code: oneOf(LINE_ITEMS).isRequired,
|
|
includeFor: arrayOf(oneOf(['customer', 'provider'])).isRequired,
|
|
quantity: instanceOf(Decimal),
|
|
unitPrice: propTypes.money.isRequired,
|
|
lineTotal: propTypes.money.isRequired,
|
|
reversal: bool.isRequired,
|
|
})
|
|
).isRequired,
|
|
transitions: arrayOf(propTypes.transition).isRequired,
|
|
}),
|
|
booking: propTypes.booking,
|
|
listing: propTypes.listing,
|
|
customer: propTypes.user,
|
|
provider: propTypes.user,
|
|
reviews: arrayOf(propTypes.review),
|
|
});
|
|
|
|
// Denormalised transaction message
|
|
propTypes.message = shape({
|
|
id: propTypes.uuid.isRequired,
|
|
type: propTypes.value('message').isRequired,
|
|
attributes: shape({
|
|
at: instanceOf(Date).isRequired,
|
|
content: string.isRequired,
|
|
}).isRequired,
|
|
sender: propTypes.user,
|
|
});
|
|
|
|
// Pagination information in the response meta
|
|
propTypes.pagination = shape({
|
|
page: number.isRequired,
|
|
perPage: number.isRequired,
|
|
totalItems: number.isRequired,
|
|
totalPages: number.isRequired,
|
|
});
|
|
|
|
export const ERROR_CODE_TRANSACTION_LISTING_NOT_FOUND = 'transaction-listing-not-found';
|
|
export const ERROR_CODE_TRANSACTION_INVALID_TRANSITION = 'transaction-invalid-transition';
|
|
export const ERROR_CODE_TRANSACTION_ALREADY_REVIEWED_BY_CUSTOMER =
|
|
'transaction-already-reviewed-by-customer';
|
|
export const ERROR_CODE_TRANSACTION_ALREADY_REVIEWED_BY_PROVIDER =
|
|
'transaction-already-reviewed-by-provider';
|
|
export const ERROR_CODE_PAYMENT_FAILED = 'transaction-payment-failed';
|
|
export const ERROR_CODE_EMAIL_TAKEN = 'email-taken';
|
|
export const ERROR_CODE_EMAIL_NOT_FOUND = 'email-not-found';
|
|
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';
|
|
const ERROR_CODES = [
|
|
ERROR_CODE_TRANSACTION_LISTING_NOT_FOUND,
|
|
ERROR_CODE_TRANSACTION_INVALID_TRANSITION,
|
|
ERROR_CODE_TRANSACTION_ALREADY_REVIEWED_BY_CUSTOMER,
|
|
ERROR_CODE_TRANSACTION_ALREADY_REVIEWED_BY_PROVIDER,
|
|
ERROR_CODE_PAYMENT_FAILED,
|
|
ERROR_CODE_EMAIL_TAKEN,
|
|
ERROR_CODE_EMAIL_NOT_FOUND,
|
|
ERROR_CODE_EMAIL_NOT_VERIFIED,
|
|
ERROR_CODE_TOO_MANY_VERIFICATION_REQUESTS,
|
|
ERROR_CODE_UPLOAD_OVER_LIMIT,
|
|
ERROR_CODE_VALIDATION_INVALID_PARAMS,
|
|
];
|
|
|
|
// API error
|
|
// TODO this is likely to change soonish
|
|
propTypes.apiError = shape({
|
|
id: propTypes.uuid.isRequired,
|
|
status: number.isRequired,
|
|
code: oneOf(ERROR_CODES).isRequired,
|
|
title: string.isRequired,
|
|
meta: object,
|
|
});
|
|
|
|
// Storable error prop type. (Error object should not be stored as it is.)
|
|
propTypes.error = shape({
|
|
type: propTypes.value('error').isRequired,
|
|
name: string.isRequired,
|
|
message: string,
|
|
status: number,
|
|
statusText: string,
|
|
apiErrors: arrayOf(propTypes.apiError),
|
|
});
|
|
|
|
export { propTypes };
|