-
Notifications
You must be signed in to change notification settings - Fork 377
/
transaction.js
349 lines (291 loc) · 12.8 KB
/
transaction.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
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-payment transition.
// At this transition a PaymentIntent is created by Marketplace API.
// After this transition, the actual payment must be made on client-side directly to Stripe.
export const TRANSITION_REQUEST_PAYMENT = 'transition/request-payment';
// 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_PAYMENT_AFTER_ENQUIRY = 'transition/request-payment-after-enquiry';
// Stripe SDK might need to ask 3D security from customer, in a separate front-end step.
// Therefore we need to make another transition to Marketplace API,
// to tell that the payment is confirmed.
export const TRANSITION_CONFIRM_PAYMENT = 'transition/confirm-payment';
// If the payment is not confirmed in the time limit set in transaction process (by default 15min)
// the transaction will expire automatically.
export const TRANSITION_EXPIRE_PAYMENT = 'transition/expire-payment';
// 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_PENDING_PAYMENT = 'pending-payment';
const STATE_PAYMENT_EXPIRED = 'payment-expired';
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: 'flex-hourly-default-process/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_PAYMENT]: STATE_PENDING_PAYMENT,
},
},
[STATE_ENQUIRY]: {
on: {
[TRANSITION_REQUEST_PAYMENT_AFTER_ENQUIRY]: STATE_PENDING_PAYMENT,
},
},
[STATE_PENDING_PAYMENT]: {
on: {
[TRANSITION_EXPIRE_PAYMENT]: STATE_PAYMENT_EXPIRED,
[TRANSITION_CONFIRM_PAYMENT]: STATE_PREAUTHORIZED,
},
},
[STATE_PAYMENT_EXPIRED]: {},
[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;
export const txIsEnquired = tx =>
getTransitionsToState(STATE_ENQUIRY).includes(txLastTransition(tx));
export const txIsPaymentPending = tx =>
getTransitionsToState(STATE_PENDING_PAYMENT).includes(txLastTransition(tx));
export const txIsPaymentExpired = tx =>
getTransitionsToState(STATE_PAYMENT_EXPIRED).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_CONFIRM_PAYMENT,
TRANSITION_DECLINE,
TRANSITION_EXPIRE,
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;
// Check if the given transition is privileged.
//
// Privileged transitions need to be handled from a secure context,
// i.e. the backend. This helper is used to check if the transition
// should go through the local API endpoints, or if using JS SDK is
// enough.
export const isPrivileged = transition => {
return [TRANSITION_REQUEST_PAYMENT, TRANSITION_REQUEST_PAYMENT_AFTER_ENQUIRY].includes(
transition
);
};