flex-template-web/src/util/transaction.js
Jenni Nurmi f340c19ae8 Use generic functions from transaction.js
(and move some generic in-file functions there).
2019-01-30 18:31:13 +02:00

314 lines
11 KiB
JavaScript

import { ensureTransaction } from './data';
/**
* Transitions
*
* These strings must sync with values defined in Flex API,
* since transaction objects given by API contain info about last transitions.
* All the actions in API side happen in transitions,
* so we need to understand what those strings mean.
*/
// 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';
/**
* Actors
*
* There are 4 different actors that might initiate transitions:
*/
// 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,
];
/**
* States
*
* These constants are only for making it clear how transitions work together.
* You should not use these constants outside of this file.
*
* Note: these states are not in sync with states used transaction process definitions
* in Marketplace API. Only last transitions are passed along transaction object.
*/
const STATE_INITIAL = 'initial';
const STATE_ENQUIRY = 'enquiry';
const STATE_PREAUTHORIZED = 'preauthorized';
const STATE_DECLINED = 'declined';
const STATE_ACCEPTED = 'accepted';
const STATE_CANCELED = 'canceled';
const STATE_DELIVERED = 'delivered';
const STATE_REVIEWED = 'reviewed';
const STATE_REVIEWED_BY_CUSTOMER = 'reviewed-by-customer';
const STATE_REVIEWED_BY_PROVIDER = 'reviewed-by-provider';
/**
* Description of transaction process
*
* You should keep this in sync with transaction process defined in Marketplace API
*
* Note: we don't use yet any state machine library,
* but this description format is following Xstate (FSM library)
* https://xstate.js.org/docs/
*/
const stateDescription = {
// id is defined only to support Xstate format.
// However if you have multiple transaction processes defined,
// it is best to keep them in sync with transaction process aliases.
id: 'preauth-with-nightly-booking/release-1',
// This 'initial' state is a starting point for new transaction
initial: STATE_INITIAL,
// States
states: {
[STATE_INITIAL]: {
on: {
[TRANSITION_ENQUIRE]: STATE_ENQUIRY,
[TRANSITION_REQUEST]: STATE_PREAUTHORIZED,
},
},
[STATE_ENQUIRY]: {
on: {
[TRANSITION_REQUEST_AFTER_ENQUIRY]: STATE_PREAUTHORIZED,
},
},
[STATE_PREAUTHORIZED]: {
on: {
[TRANSITION_DECLINE]: STATE_DECLINED,
[TRANSITION_EXPIRE]: STATE_DECLINED,
[TRANSITION_ACCEPT]: STATE_ACCEPTED,
},
},
[STATE_DECLINED]: {},
[STATE_ACCEPTED]: {
on: {
[TRANSITION_CANCEL]: STATE_CANCELED,
[TRANSITION_COMPLETE]: STATE_DELIVERED,
},
},
[STATE_CANCELED]: {},
[STATE_DELIVERED]: {
on: {
[TRANSITION_EXPIRE_REVIEW_PERIOD]: STATE_REVIEWED,
[TRANSITION_REVIEW_1_BY_CUSTOMER]: STATE_REVIEWED_BY_CUSTOMER,
[TRANSITION_REVIEW_1_BY_PROVIDER]: STATE_REVIEWED_BY_PROVIDER,
},
},
[STATE_REVIEWED_BY_CUSTOMER]: {
on: {
[TRANSITION_REVIEW_2_BY_PROVIDER]: STATE_REVIEWED,
[TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD]: STATE_REVIEWED,
},
},
[STATE_REVIEWED_BY_PROVIDER]: {
on: {
[TRANSITION_REVIEW_2_BY_CUSTOMER]: STATE_REVIEWED,
[TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD]: STATE_REVIEWED,
},
},
[STATE_REVIEWED]: { type: 'final' },
},
};
// Note: currently we assume that state description doesn't contain nested states.
const statesFromStateDescription = description => description.states || {};
// Get all the transitions from states object in an array
const getTransitions = states => {
const stateNames = Object.keys(states);
const transitionsReducer = (transitionArray, name) => {
const stateTransitions = states[name] && states[name].on;
const transitionKeys = stateTransitions ? Object.keys(stateTransitions) : [];
return [
...transitionArray,
...transitionKeys.map(key => ({ key, value: stateTransitions[key] })),
];
};
return stateNames.reduce(transitionsReducer, []);
};
// This is a list of all the transitions that this app should be able to handle.
export const TRANSITIONS = getTransitions(statesFromStateDescription(stateDescription)).map(
t => t.key
);
// This function returns a function that has given stateDesc in scope chain.
const getTransitionsToStateFn = stateDesc => state =>
getTransitions(statesFromStateDescription(stateDesc))
.filter(t => t.value === state)
.map(t => t.key);
// Get all the transitions that lead to specified state.
const getTransitionsToState = getTransitionsToStateFn(stateDescription);
// This is needed to fetch transactions that need response from provider.
// I.e. transactions which provider needs to accept or decline
export const transitionsToRequested = getTransitionsToState(STATE_PREAUTHORIZED);
/**
* Helper functions to figure out if transaction is in a specific state.
* State is based on lastTransition given by transaction object and state description.
*/
const txLastTransition = tx => ensureTransaction(tx).attributes.lastTransition;
// DEPRECATED: use txIsDelivered instead
export const txIsCompleted = tx => txLastTransition(tx) === TRANSITION_COMPLETE;
export const txIsEnquired = tx =>
getTransitionsToState(STATE_ENQUIRY).includes(txLastTransition(tx));
// Note: state name used in Marketplace API docs (and here) is actually preauthorized
// However, word "requested" is used in many places so that we decided to keep it.
export const txIsRequested = tx =>
getTransitionsToState(STATE_PREAUTHORIZED).includes(txLastTransition(tx));
export const txIsAccepted = tx =>
getTransitionsToState(STATE_ACCEPTED).includes(txLastTransition(tx));
export const txIsDeclined = tx =>
getTransitionsToState(STATE_DECLINED).includes(txLastTransition(tx));
export const txIsCanceled = tx =>
getTransitionsToState(STATE_CANCELED).includes(txLastTransition(tx));
export const txIsDelivered = tx =>
getTransitionsToState(STATE_DELIVERED).includes(txLastTransition(tx));
const firstReviewTransitions = [
...getTransitionsToState(STATE_REVIEWED_BY_CUSTOMER),
...getTransitionsToState(STATE_REVIEWED_BY_PROVIDER),
];
export const txIsInFirstReview = tx => firstReviewTransitions.includes(txLastTransition(tx));
export const txIsInFirstReviewBy = (tx, isCustomer) =>
isCustomer
? getTransitionsToState(STATE_REVIEWED_BY_CUSTOMER).includes(txLastTransition(tx))
: getTransitionsToState(STATE_REVIEWED_BY_PROVIDER).includes(txLastTransition(tx));
export const txIsReviewed = tx =>
getTransitionsToState(STATE_REVIEWED).includes(txLastTransition(tx));
/**
* Helper functions to figure out if transaction has passed a given state.
* This is based on transitions history given by transaction object.
*/
const txTransitions = tx => ensureTransaction(tx).attributes.transitions || [];
const hasPassedTransition = (transitionName, tx) =>
!!txTransitions(tx).find(t => t.transition === transitionName);
const hasPassedStateFn = state => tx =>
getTransitionsToState(state).filter(t => hasPassedTransition(t, tx)).length > 0;
export const txHasBeenAccepted = hasPassedStateFn(STATE_ACCEPTED);
export const txHasBeenDelivered = hasPassedStateFn(STATE_DELIVERED);
/**
* Other transaction related utility functions
*/
export const transitionIsReviewed = transition =>
getTransitionsToState(STATE_REVIEWED).includes(transition);
export const transitionIsFirstReviewedBy = (transition, isCustomer) =>
isCustomer
? getTransitionsToState(STATE_REVIEWED_BY_CUSTOMER).includes(transition)
: getTransitionsToState(STATE_REVIEWED_BY_PROVIDER).includes(transition);
export const getReview1Transition = isCustomer =>
isCustomer ? TRANSITION_REVIEW_1_BY_CUSTOMER : TRANSITION_REVIEW_1_BY_PROVIDER;
export const getReview2Transition = isCustomer =>
isCustomer ? TRANSITION_REVIEW_2_BY_CUSTOMER : TRANSITION_REVIEW_2_BY_PROVIDER;
// Check if a transition is the kind that should be rendered
// when showing transition history (e.g. ActivityFeed)
// The first transition and most of the expiration transitions made by system are not relevant
export const isRelevantPastTransition = transition => {
return [
TRANSITION_ACCEPT,
TRANSITION_CANCEL,
TRANSITION_COMPLETE,
TRANSITION_DECLINE,
TRANSITION_EXPIRE,
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,
].includes(transition);
};
export const isCustomerReview = transition => {
return [TRANSITION_REVIEW_1_BY_CUSTOMER, TRANSITION_REVIEW_2_BY_CUSTOMER].includes(transition);
};
export const isProviderReview = transition => {
return [TRANSITION_REVIEW_1_BY_PROVIDER, TRANSITION_REVIEW_2_BY_PROVIDER].includes(transition);
};
export const getUserTxRole = (currentUserId, transaction) => {
const tx = ensureTransaction(transaction);
const customer = tx.customer;
if (currentUserId && currentUserId.uuid && tx.id && customer.id) {
// user can be either customer or provider
return currentUserId.uuid === customer.id.uuid
? TX_TRANSITION_ACTOR_CUSTOMER
: TX_TRANSITION_ACTOR_PROVIDER;
} else {
throw new Error(`Parameters for "userIsCustomer" function were wrong.
currentUserId: ${currentUserId}, transaction: ${transaction}`);
}
};
export const txRoleIsProvider = userRole => userRole === TX_TRANSITION_ACTOR_PROVIDER;
export const txRoleIsCustomer = userRole => userRole === TX_TRANSITION_ACTOR_CUSTOMER;