Commit 1b809bfd authored by Dmytro Zavgorodniy's avatar Dmytro Zavgorodniy

[RNA-761] Add latest actions

parent 1d60a377
export const ANALYTICS_LOG_EVENT = 'ANALYTICS_LOG_EVENT'
export function analyticsLogEvent(eventName, params) {
return { type: ANALYTICS_LOG_EVENT, payload: { eventName, params } }
}
export default function createAsyncAction(types, callback) { export default function createAsyncAction(types, callback) {
return payload => async (dispatch) => { return payload => async (dispatch, getState) => {
const [requestType, successType, failureType] = types const [requestType, successType, failureType] = types
dispatch({ type: requestType, payload }) dispatch({ type: requestType, payload })
try { try {
await callback(dispatch, payload) await callback(dispatch, getState, payload)
} catch (error) { } catch (error) {
return dispatch({ type: failureType, payload: { ...payload, error } }) return dispatch({ type: failureType, payload: { ...payload, error } })
} }
......
...@@ -5,6 +5,8 @@ export * from './storeFilters' ...@@ -5,6 +5,8 @@ export * from './storeFilters'
export * from './storeCarts' export * from './storeCarts'
export * from './userCards' export * from './userCards'
export * from './userAddresses' export * from './userAddresses'
export * from './userContacts'
export * from './payments' export * from './payments'
export * from './userAgeConfirmation' export * from './userAgeConfirmation'
export * from './cleanReducers' export * from './cleanReducers'
export * from './analytics'
import stripe from 'tipsi-stripe' import stripe from 'tipsi-stripe'
import { get } from 'lodash' import { get } from 'lodash'
import { performPayment, loadOrder, updateOrder } from 'tipsi_api/actions' import { payStoreCart, loadOrder, updateOrder, loadDeliveryZone } from 'tipsi_api/actions'
import { createOrderSelector } from 'tipsi_api/selectors' import { createStoreCartSelector, userSelector } from 'tipsi_api/selectors'
import { createAsyncConstants } from 'tipsi_api/utils' import { createAsyncConstants } from 'tipsi_api/utils'
import { APPLE_PAY, ANDROID_PAY } from '../constants/paymentMethods' import { analyticsLogEvent, loadStoreCart } from '../actions'
import { APPLE_PAY, ANDROID_PAY, CREDIT_CARD } from '../constants/paymentMethods'
import { repeat, geometricPause } from '../utils/sequentional' import { repeat, geometricPause } from '../utils/sequentional'
import { applePayOrderFormatter, androidPayOrderFormatter } from '../utils/formatters' import { applePayOrderFormatter, androidPayOrderFormatter } from '../utils/formatters'
import getOrderProductsCategories from '../utils/getOrderProductsCategories'
import isDeliveryPossible from '../utils/isDeliveryPossible'
export const PaymentsActionTypes = createAsyncConstants(['REQUEST_PAYMENT', 'FINALIZE_PAYMENT']) const PaymentsActionTypes = createAsyncConstants(['REQUEST_PAYMENT', 'FINALIZE_PAYMENT'])
const TIMES_TO_REPEAT = 10 const TIMES_TO_REPEAT = 10
const PAUSE_AFTER_REPEAT = geometricPause(300) const PAUSE_AFTER_REPEAT = geometricPause(300)
const TIMEOUT_ERROR_TEXT = 'Error while placing order, try again in 30 sec.' const TIMEOUT_ERROR_TEXT = 'Error while placing order, try again in 30 sec.'
const ORDER_FIELDS = { const ORDER_FIELDS = {
order: 'id,created,updated,code,order_status,payment_status,store,total_price,total_count', order: 'id,created,updated,code,order_status,payment_status,store,total_price,total_count,' +
'payment_error',
} }
/** /**
* returns true if we need to check order status again * returns true if we need to check order status again
...@@ -44,48 +48,133 @@ function generatePaymentToken(order, method, card) { ...@@ -44,48 +48,133 @@ function generatePaymentToken(order, method, card) {
return stripe.createTokenWithCard(card) return stripe.createTokenWithCard(card)
} }
export const requestPayment = (orderId, deliveryType) => async (dispatch, getState) => { const requestPayment = (storeId, deliveryType) => async (dispatch, getState) => {
try { try {
dispatch({ type: PaymentsActionTypes.REQUEST_PAYMENT.LOADING, payload: { orderId } }) dispatch({ type: PaymentsActionTypes.REQUEST_PAYMENT.LOADING, payload: { storeId } })
// Load current Store Card
await dispatch(loadStoreCart(storeId))
// Load order from REDUX // Load order from REDUX
const order = createOrderSelector(() => orderId)(getState()).order const { orderId, order } = createStoreCartSelector(() => storeId)(getState())
// Update order // Update order
if (order.order_status !== 'placed') { if (order.delivery_type !== deliveryType) {
await dispatch(updateOrder(orderId, { order_status: 'placed', delivery_type: deliveryType })) await dispatch(updateOrder(orderId, { delivery_type: deliveryType }))
} }
dispatch({ type: PaymentsActionTypes.REQUEST_PAYMENT.SUCCESS, payload: { orderId } }) dispatch({ type: PaymentsActionTypes.REQUEST_PAYMENT.SUCCESS, payload: { storeId } })
} catch (error) { } catch (error) {
dispatch({ type: PaymentsActionTypes.REQUEST_PAYMENT.ERROR, payload: { orderId, error } }) dispatch({ type: PaymentsActionTypes.REQUEST_PAYMENT.ERROR, payload: { storeId, error } })
throw error throw error
} }
} }
export const finalizePayment = (orderId, storeId, method, card) => async (dispatch, getState) => { const finalizePayment = (storeId, method, card, address, contact) => async (dispatch, getState) => {
// Load order from REDUX
const state = getState()
const { orderId, order, orderProducts } = createStoreCartSelector(() => storeId)(state)
const { user } = userSelector(state)
// Analytics
const eventParams = {
paymentMethod: method,
storeId,
orderId,
userId: user.id,
totalCount: order.total_count,
totalPrice: order.total_price,
...getOrderProductsCategories(orderProducts),
}
try { try {
dispatch({ type: PaymentsActionTypes.FINALIZE_PAYMENT.LOADING, payload: { orderId } }) dispatch({ type: PaymentsActionTypes.FINALIZE_PAYMENT.LOADING, payload: { storeId } })
// Load order from REDUX // Analytics
const order = createOrderSelector(() => orderId)(getState()).order dispatch(analyticsLogEvent('non_ui_initiatePayment', eventParams))
// Generate payment token // Generate payment token
const paymentResult = await generatePaymentToken(order, method, card) const paymentResult = await generatePaymentToken(order, method, card)
const extraCheckMethods = [APPLE_PAY, ANDROID_PAY]
let deliveryInfo = {}
if (order.delivery_type === 'delivery') {
const shippingContact = address || get(paymentResult, 'extra.shippingContact')
let deliveryAddress = {}
switch (method) {
case APPLE_PAY:
deliveryAddress = {
address1: shippingContact.street,
city: shippingContact.city,
state: shippingContact.state,
postalCode: shippingContact.postalCode,
}
break
case ANDROID_PAY:
deliveryAddress = {
address1: shippingContact.address1,
address2: shippingContact.address2,
city: shippingContact.locality,
state: shippingContact.administrativeArea,
postalCode: shippingContact.postalCode,
}
break
case CREDIT_CARD:
deliveryAddress = {
address1: address.street,
state: address.state,
postalCode: address.zip,
city: address.city,
}
break
default:
}
// Update order if (extraCheckMethods.includes(method)) {
const phone = get(paymentResult, 'extra.shippingContact.phoneNumber') const { payload } = await dispatch(loadDeliveryZone(storeId, deliveryAddress))
if (phone) {
await dispatch(updateOrder(orderId, { phone })) const { extra, lat, lng } = payload.result
const { possible, error } = isDeliveryPossible(order.products_price, extra.zone)
if (!possible) {
const { address1, city, state: shippingState, postalCode } = deliveryAddress
const errorMessage = {
code: 'incorrectShippingAddress',
error,
info: {
address: `${address1}\n${city}\n${shippingState} ${postalCode}`,
location: { latitude: lat, longitude: lng },
zone: extra.zone,
},
}
throw errorMessage
}
}
deliveryInfo = {
name: address ? address.name : shippingContact.name,
phone: address ? address.phone : shippingContact.phoneNumber,
state: deliveryAddress.state,
street_address_1: deliveryAddress.address1,
street_address_2: deliveryAddress.address2,
city: deliveryAddress.city,
zip_code: deliveryAddress.postalCode,
}
} else {
const phoneSource = (order.delivery_type === 'delivery' ? address : contact) || {}
const phone = phoneSource.phone || get(paymentResult, 'extra.shippingContact.phoneNumber')
deliveryInfo = { phone }
} }
// Perform payment // Perform payment
const { tokenId: paymentTokenId } = paymentResult const { tokenId: paymentTokenId } = paymentResult
await dispatch(performPayment(storeId, orderId, { await dispatch(payStoreCart(storeId, {
provider: 'stripe', provider: 'stripe',
usd_amount: order.total_price, usd_amount: order.total_price,
data: { source: paymentTokenId }, data: { source: paymentTokenId },
delivery_info: deliveryInfo,
})) }))
// Check order status // Check order status
...@@ -102,19 +191,36 @@ export const finalizePayment = (orderId, storeId, method, card) => async (dispat ...@@ -102,19 +191,36 @@ export const finalizePayment = (orderId, storeId, method, card) => async (dispat
await stripe.completeApplePayRequest() await stripe.completeApplePayRequest()
} }
dispatch({ type: PaymentsActionTypes.FINALIZE_PAYMENT.SUCCESS, payload: { orderId } }) // Analytics
dispatch(analyticsLogEvent('non_ui_completedPayment', eventParams))
dispatch({ type: PaymentsActionTypes.FINALIZE_PAYMENT.SUCCESS, payload: { storeId } })
return createStoreCartSelector(() => storeId)(state).order
} catch (error) { } catch (error) {
let userCanceled = false
// Cancel Apple Pay request // Cancel Apple Pay request
if (method === APPLE_PAY) { if (method === APPLE_PAY) {
const { TPSErrorDomain, TPSErrorCodeUserCancel } = stripe const { TPSErrorDomain, TPSErrorCodeUserCancel } = stripe
const userCanceled = error.domain === TPSErrorDomain && error.code === TPSErrorCodeUserCancel userCanceled = error.domain === TPSErrorDomain && error.code === TPSErrorCodeUserCancel
if (!userCanceled) { if (!userCanceled) {
await stripe.cancelApplePayRequest() await stripe.cancelApplePayRequest()
} }
} }
dispatch({ type: PaymentsActionTypes.FINALIZE_PAYMENT.ERROR, payload: { orderId, error } }) // Analytics
const eventName = userCanceled ? 'non_ui_canceledPayment' : 'non_ui_errorPayment'
dispatch(analyticsLogEvent(eventName, eventParams))
dispatch({ type: PaymentsActionTypes.FINALIZE_PAYMENT.ERROR, payload: { storeId, error } })
throw error throw error
} }
} }
export {
PaymentsActionTypes,
finalizePayment,
requestPayment,
}
import { get, omit } from 'lodash' import { loadStoreCart as loadCart } from 'tipsi_api/actions'
import {
createStoreSelector,
createOrderSelector,
createStoreCartOrderSelector,
createOrderProductsSelector,
createInventoryOrderProductSelector,
} from 'tipsi_api/selectors'
import {
loadStore,
loadStoreCartOrder,
createOrder,
addProductToOrder,
loadOrder,
updateProductCount,
removeProduct,
loadProducts,
} from 'tipsi_api/actions'
import { createAsyncConstants } from 'tipsi_api/utils'
import createAsyncAction from './createAsyncAction'
import { defaultDeliveryType } from '../utils/deliveryTypes'
export const ActionTypes = createAsyncConstants([ export const MODIFY_STORE_CART = 'MODIFY_STORE_CART'
'LOAD_STORE_CART',
'ADD_INVENTORY_TO_CART',
'INCREASE_INVENTORY_COUNT',
'DECREASE_INVENTORY_COUNT',
'REMOVE_INVENTORY_FROM_CART',
])
export const loadStoreCart = createAsyncAction( /* eslint-disable import/prefer-default-export */
ActionTypes.LOAD_STORE_CART, export const loadStoreCart = storeId => (dispatch) => {
async (dispatch, { storeId }) => { const fields = {
await dispatch(loadStoreIfNeeded(storeId)) order: 'id,total_count,order_status,code,' +
await dispatch(loadOrderIfNeeded(storeId)) 'products_price,products_tax,products_discount,delivery_price,delivery_tax,total_price',
await dispatch(loadOrderProductsIfNeeded(storeId)) product: 'id,count,total_price,inventory,wine,drink',
inventory: 'id,in_stock,price,pack_size',
wine: 'id,name,label_url,vintage',
drink: 'id,name,label_url',
} }
) return dispatch(loadCart(storeId, fields))
export const addInventoryToCart = createAsyncAction(
ActionTypes.ADD_INVENTORY_TO_CART,
async (dispatch, { storeId, inventoryId }) => {
await dispatch(loadStoreIfNeeded(storeId))
await dispatch(loadOrderIfNeeded(storeId))
const order = await dispatch(createOrderIfNeeded(storeId))
await dispatch(addInventoryToCartIfNeeded(storeId, inventoryId))
await dispatch(loadOrderProductsIfPossible(storeId))
await dispatch(reloadOrder(order.id))
}
)
export const increaseInventoryCount = createAsyncAction(
ActionTypes.INCREASE_INVENTORY_COUNT,
async (dispatch, { storeId, inventoryId }) => {
await dispatch(loadStoreIfNeeded(storeId))
const order = await dispatch(loadOrderIfNeeded(storeId))
await dispatch(increaseInventoryCountIfNeeded(storeId, inventoryId))
await dispatch(reloadOrder(order.id))
}
)
export const decreaseInventoryCount = createAsyncAction(
ActionTypes.DECREASE_INVENTORY_COUNT,
async (dispatch, { storeId, inventoryId }) => {
await dispatch(loadStoreIfNeeded(storeId))
const order = await dispatch(loadOrderIfNeeded(storeId))
await dispatch(decreaseInventoryCountIfNeeded(storeId, inventoryId))
await dispatch(reloadOrder(order.id))
}
)
export const removeInventoryFromCart = createAsyncAction(
ActionTypes.REMOVE_INVENTORY_FROM_CART,
async (dispatch, { storeId, inventoryId }) => {
await dispatch(loadStoreIfNeeded(storeId))
const order = await dispatch(loadOrderIfNeeded(storeId))
await dispatch(removeInventoryFromCartIfNeeded(storeId, inventoryId))
await dispatch(reloadOrder(order.id))
}
)
function lookupStoreById(state, storeId) {
return createStoreSelector(() => storeId)(state).store
}
function lookupOrderById(state, orderId) {
return createOrderSelector(() => orderId)(state).order
}
function lookupOrder(state, storeId) {
return createStoreCartOrderSelector(() => storeId)(state)
}
function lookupOrderProduct(state, storeId, inventoryId) {
const order = lookupOrder(state, storeId) || {}
return createInventoryOrderProductSelector(() => order.id, () => inventoryId)(state)
}
function lookupOrderProducts(state, storeId) {
const order = lookupOrder(state, storeId) || {}
return createOrderProductsSelector(() => order.id)(state).orderProducts
}
const loadStoreIfNeeded = storeId => async (dispatch, getState) => {
let store = lookupStoreById(getState(), storeId)
if (!store || store.orders_enabled === undefined) {
await dispatch(loadStore(storeId))
store = lookupStoreById(getState(), storeId)
}
return store
} }
const loadOrderIfNeeded = storeId => async (dispatch, getState) => { export function modifyStoreCart(storeId, product) {
const state = getState() return { type: MODIFY_STORE_CART, payload: { storeId, product } }
const store = lookupStoreById(state, storeId)
let order = lookupOrder(state, storeId)
if (store && store.orders_enabled && !order) {
await dispatch(loadStoreCartOrder({
storeId,
fields: {
order: 'id,store,total_count',
store: 'id,orders_enabled',
},
}))
order = lookupOrder(getState(), storeId)
}
return order
}
const loadOrderProductsIfPossible = storeId => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
const order = lookupOrder(state, storeId)
let orderProducts = []
if (store && store.orders_enabled && order) {
let page = 1
let shouldLoadNextPage = true
while (shouldLoadNextPage) {
const resultAction = await dispatch(loadProducts({
orderId: order.id,
fields: {
product: 'id,count,total_price,wine,drink,inventory',
inventory: 'id,price,special_price_on,special_price,special_price_amount,in_stock',
wine: 'id,name,vintage,label_url',
drink: 'id,name,label_url',
},
page,
}))
page += 1
shouldLoadNextPage = !!get(resultAction, 'payload.result.next')
}
orderProducts = lookupOrderProducts(getState(), storeId)
}
return orderProducts
}
const loadOrderProductsIfNeeded = storeId => async (dispatch, getState) => {
let orderProducts = lookupOrderProducts(getState(), storeId)
if (orderProducts && orderProducts.length === 0) {
await dispatch(loadOrderProductsIfPossible(storeId))
orderProducts = lookupOrderProducts(getState(), storeId)
}
return orderProducts
}
let createOrderPromises = {}
const createOrderIfNeeded = storeId => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
let order = lookupOrder(state, storeId)
if (store && store.orders_enabled && !order) {
let createOrderPromise = createOrderPromises[storeId]
if (!createOrderPromise) {
createOrderPromise = dispatch(createOrder({
store_id: storeId,
delivery_type: defaultDeliveryType,
}))
createOrderPromises[storeId] = createOrderPromise
}
await createOrderPromise
createOrderPromises = omit(createOrderPromises, storeId)
order = lookupOrder(getState(), storeId)
}
return order
}
const addInventoryToCartIfNeeded = (storeId, inventoryId) => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
const order = lookupOrder(state, storeId)
let orderProduct
if (store && store.orders_enabled && order) {
await dispatch(addProductToOrder({
orderId: order.id,
data: {
inventory_id: inventoryId,
count: 1,
},
}))
orderProduct = lookupOrderProduct(getState(), storeId, inventoryId)
}
return orderProduct
}
const reloadOrder = orderId => async (dispatch, getState) => {
await dispatch(loadOrder({
orderId,
fields: {
order: 'id,total_count,order_status,' +
'products_price,products_tax,products_discount,delivery_price,delivery_tax,total_price,' +
'store',
store: 'id,name,orders_enabled',
},
}))
return lookupOrderById(getState(), orderId)
}
const increaseInventoryCountIfNeeded = (storeId, inventoryId) => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
const order = lookupOrder(state, storeId)
let orderProduct = lookupOrderProduct(state, storeId, inventoryId)
if (store && store.orders_enabled && order && orderProduct) {
const count = orderProduct.count + 1
await dispatch(updateProductCount({
orderId: order.id,
productId: orderProduct.id,
count,
}))
orderProduct = lookupOrderProduct(getState(), storeId, inventoryId)
}
return orderProduct
}
const decreaseInventoryCountIfNeeded = (storeId, inventoryId) => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
const order = lookupOrder(state, storeId)
let orderProduct = lookupOrderProduct(state, storeId, inventoryId)
if (store && store.orders_enabled && order && orderProduct) {
const count = orderProduct.count - 1
if (count > 0) {
await dispatch(updateProductCount({
orderId: order.id,
productId: orderProduct.id,
count,
}))
} else {
await dispatch(removeProduct({
orderId: order.id,
productId: orderProduct.id,
}))
}
orderProduct = lookupOrderProduct(getState(), storeId, inventoryId)
}
return orderProduct
}
const removeInventoryFromCartIfNeeded = (storeId, inventoryId) => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
const order = lookupOrder(state, storeId)
const orderProduct = lookupOrderProduct(state, storeId, inventoryId)
if (store && store.orders_enabled && order && orderProduct) {
await dispatch(removeProduct({
orderId: order.id,
productId: orderProduct.id,
}))
}
return null
} }
export const ADD_CONTACT = 'ADD_CONTACT'
export const REMOVE_CONTACT = 'REMOVE_CONTACT'
export const SELECT_CONTACT = 'SELECT_CONTACT'
export function addContact(id, phone) {
return {
type: ADD_CONTACT,
payload: {
id,
phone,
},
}
}
export function removeContact(id) {
return {
type: REMOVE_CONTACT,
payload: {
id,
},
}
}
export function selectContact(id) {
return {
type: SELECT_CONTACT,
payload: {
id,
},
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment