diff --git a/src/actions/__tests__/payment-profile-actions.test.js b/src/actions/__tests__/payment-profile-actions.test.js new file mode 100644 index 000000000..7d309bebf --- /dev/null +++ b/src/actions/__tests__/payment-profile-actions.test.js @@ -0,0 +1,157 @@ +import configureStore from "redux-mock-store"; +import thunk from "redux-thunk"; +import flushPromises from "flush-promises"; +import { getRequest } from "openstack-uicore-foundation/lib/utils/actions"; +import { getPaymentProfiles } from "../ticket-actions"; +import * as methods from "../../utils/methods"; + +jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({ + __esModule: true, + ...jest.requireActual("openstack-uicore-foundation/lib/utils/actions"), + getRequest: jest.fn() +})); + +describe("getPaymentProfiles", () => { + const middlewares = [thunk]; + const mockStore = configureStore(middlewares); + const SUMMIT_ID = 42; + let store; + let capturedParams; + + beforeEach(() => { + jest.clearAllMocks(); + capturedParams = null; + store = mockStore({ + currentSummitState: { currentSummit: { id: SUMMIT_ID } } + }); + jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN"); + window.PURCHASES_API_URL = "https://purchases.example.com"; + + getRequest.mockImplementation( + (reqAC, recAC, url, errHandler, extraPayload) => (params) => { + capturedParams = params; + return (dispatch) => { + dispatch(reqAC(extraPayload || {})); + return Promise.resolve().then(() => { + dispatch( + recAC({ + response: { + data: [], + total: 0, + current_page: 1, + last_page: 1 + } + }) + ); + }); + }; + } + ); + }); + + afterEach(() => { + jest.restoreAllMocks(); + delete window.PURCHASES_API_URL; + }); + + test("dispatches START_LOADING, REQUEST, RECEIVE, STOP_LOADING", async () => { + store.dispatch(getPaymentProfiles()); + await flushPromises(); + + const types = store.getActions().map((a) => a.type); + expect(types).toEqual([ + "START_LOADING", + "REQUEST_PAYMENT_PROFILES", + "RECEIVE_PAYMENT_PROFILES", + "STOP_LOADING" + ]); + expect(getRequest).toHaveBeenCalledTimes(1); + }); + + test("builds correct summit URL", async () => { + store.dispatch(getPaymentProfiles()); + await flushPromises(); + + const url = getRequest.mock.calls[0][2]; + expect(url).toBe( + `https://purchases.example.com/api/v1/summits/${SUMMIT_ID}/payment-profiles` + ); + }); + + test.each(["", undefined])("omits filter[] when term is %p", async (term) => { + store.dispatch(getPaymentProfiles(term)); + await flushPromises(); + + expect(capturedParams).not.toHaveProperty("filter[]"); + }); + + test("adds provider and application_type string filters for non-numeric term", async () => { + store.dispatch(getPaymentProfiles("stripe")); + await flushPromises(); + + expect(capturedParams["filter[]"]).toEqual([ + "provider=@stripe,application_type=@stripe" + ]); + }); + + test("adds exact-match id filter alongside string filters for numeric term", async () => { + store.dispatch(getPaymentProfiles("42")); + await flushPromises(); + + expect(capturedParams["filter[]"]).toEqual([ + "provider=@42,application_type=@42", + "id==42" + ]); + }); + + test.each([ + ["foo,bar", "provider=@foo\\,bar,application_type=@foo\\,bar"], + ["foo;bar", "provider=@foo\\;bar,application_type=@foo\\;bar"] + ])( + "escapeFilterValue escapes special chars in %p", + async (term, expected) => { + store.dispatch(getPaymentProfiles(term)); + await flushPromises(); + + expect(capturedParams["filter[]"]).toEqual([expected]); + } + ); + + test("REQUEST_PAYMENT_PROFILES payload includes term, page, perPage, order, orderDir", async () => { + store.dispatch(getPaymentProfiles("stripe", 2, 25, "provider", 0)); + await flushPromises(); + + const extraPayload = getRequest.mock.calls[0][4]; + expect(extraPayload).toEqual({ + term: "stripe", + page: 2, + perPage: 25, + order: "provider", + orderDir: 0 + }); + }); + + test("params include access_token, page, and per_page", async () => { + store.dispatch(getPaymentProfiles("", 3, 20)); + await flushPromises(); + + expect(capturedParams).toMatchObject({ + access_token: "TOKEN", + page: 3, + per_page: 20 + }); + }); + + test.each([ + [1, "+provider"], + [0, "-provider"] + ])( + "order param uses correct prefix for orderDir %i", + async (orderDir, expected) => { + store.dispatch(getPaymentProfiles("", 1, 10, "provider", orderDir)); + await flushPromises(); + + expect(capturedParams.order).toBe(expected); + } + ); +}); diff --git a/src/actions/ticket-actions.js b/src/actions/ticket-actions.js index 3730f26ce..444fe6480 100644 --- a/src/actions/ticket-actions.js +++ b/src/actions/ticket-actions.js @@ -47,6 +47,7 @@ import { EXPORT_PAGE_SIZE_100, TEN } from "../utils/constants"; +import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions"; export const REQUEST_TICKETS = "REQUEST_TICKETS"; export const RECEIVE_TICKETS = "RECEIVE_TICKETS"; @@ -1186,6 +1187,7 @@ export const deleteRefundPolicy = export const getPaymentProfiles = ( + term = "", page = DEFAULT_CURRENT_PAGE, perPage = DEFAULT_PER_PAGE, order = "id", @@ -1195,6 +1197,7 @@ export const getPaymentProfiles = const { currentSummitState } = getState(); const accessToken = await getAccessTokenSafely(); const { currentSummit } = currentSummitState; + const filter = []; dispatch(startLoading()); @@ -1204,19 +1207,32 @@ export const getPaymentProfiles = access_token: accessToken }; + if (term) { + const escapedTerm = escapeFilterValue(term); + filter.push(`provider=@${escapedTerm},application_type=@${escapedTerm}`); + const numericId = parseInt(escapedTerm, 10); + if (!Number.isNaN(numericId)) { + filter.push(`id==${numericId}`); + } + } + // order if (order != null && orderDir != null) { const orderDirSign = orderDir === DEFAULT_ORDER_DIR ? "+" : "-"; params.order = `${orderDirSign}${order}`; } + if (filter.length > 0) { + params["filter[]"] = filter; + } + return getRequest( createAction(REQUEST_PAYMENT_PROFILES), createAction(RECEIVE_PAYMENT_PROFILES), `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles`, - authErrorHandler, - { page, perPage, order, orderDir } + snackbarErrorHandler, + { term, page, perPage, order, orderDir } )(params)(dispatch).then(() => { dispatch(stopLoading()); }); @@ -1227,49 +1243,48 @@ export const savePaymentProfile = (entity) => async (dispatch, getState) => { const accessToken = await getAccessTokenSafely(); const { currentSummit } = currentSummitState; + dispatch(startLoading()); + const params = { access_token: accessToken }; if (entity.id) { - putRequest( + return putRequest( createAction(UPDATE_PAYMENT_PROFILE), createAction(PAYMENT_PROFILE_UPDATED), `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${entity.id}`, entity, - authErrorHandler, + snackbarErrorHandler, entity - )(params)(dispatch).then(() => { - dispatch( - showSuccessMessage( - T.translate("edit_payment_profile.payment_profile_saved") - ) - ); - }); - return; + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("edit_payment_profile.payment_profile_saved") + }) + ); + }) + .finally(() => dispatch(stopLoading())); } - const success_message = { - title: T.translate("general.done"), - html: T.translate("edit_payment_profile.payment_profile_created"), - type: "success" - }; - - postRequest( + return postRequest( createAction(UPDATE_PAYMENT_PROFILE), createAction(PAYMENT_PROFILE_ADDED), `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles`, entity, - authErrorHandler - )(params)(dispatch).then((payload) => { - dispatch( - showMessage(success_message, () => { - history.push( - `/app/summits/${currentSummit.id}/payment-profiles/${payload.response.id}` - ); - }) - ); - }); + snackbarErrorHandler + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("edit_payment_profile.payment_profile_created") + }) + ); + }) + .finally(() => dispatch(stopLoading())); }; export const deletePaymentProfile = @@ -1287,7 +1302,7 @@ export const deletePaymentProfile = createAction(PAYMENT_PROFILE_DELETED)({ paymentProfileId }), `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}`, null, - authErrorHandler + snackbarErrorHandler )(params)(dispatch).then(() => { dispatch(stopLoading()); }); @@ -1309,7 +1324,7 @@ export const getPaymentProfile = null, createAction(RECEIVE_PAYMENT_PROFILE), `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}`, - authErrorHandler + snackbarErrorHandler )(params)(dispatch).then(() => { dispatch(stopLoading()); }); @@ -1347,7 +1362,7 @@ export const getPaymentFeeTypes = createAction(RECEIVE_PAYMENT_FEE_TYPES), `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}/fee-types`, - authErrorHandler, + snackbarErrorHandler, { page, perPage, order, orderDir } )(params)(dispatch).then(() => { dispatch(stopLoading()); @@ -1366,45 +1381,44 @@ export const savePaymentFeeType = (entity) => async (dispatch, getState) => { access_token: accessToken }; + dispatch(startLoading()); + if (entity.id) { - putRequest( + return putRequest( createAction(UPDATE_PAYMENT_FEE_TYPE), createAction(PAYMENT_FEE_TYPE_UPDATED), `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}/fee-types/${entity.id}`, entity, - authErrorHandler, + snackbarErrorHandler, entity - )(params)(dispatch).then(() => { - dispatch( - showSuccessMessage( - T.translate("edit_payment_fee_type.payment_fee_type_saved") - ) - ); - }); - return; + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("edit_payment_fee_type.payment_fee_type_saved") + }) + ); + }) + .finally(() => dispatch(stopLoading())); } - const success_message = { - title: T.translate("general.done"), - html: T.translate("edit_payment_fee_type.payment_fee_type_created"), - type: "success" - }; - - postRequest( + return postRequest( createAction(UPDATE_PAYMENT_FEE_TYPE), createAction(PAYMENT_FEE_TYPE_ADDED), `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}/fee-types`, entity, - authErrorHandler - )(params)(dispatch).then(() => { - dispatch( - showMessage(success_message, () => { - history.push( - `/app/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}` - ); - }) - ); - }); + snackbarErrorHandler + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("edit_payment_fee_type.payment_fee_type_created") + }) + ); + }) + .finally(() => dispatch(stopLoading())); }; export const deletePaymentFeeType = @@ -1425,7 +1439,7 @@ export const deletePaymentFeeType = createAction(PAYMENT_FEE_TYPE_DELETED)({ paymentFeeTypeId }), `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}/fee-types/${paymentFeeTypeId}`, null, - authErrorHandler + snackbarErrorHandler )(params)(dispatch).then(() => { dispatch(stopLoading()); }); @@ -1450,7 +1464,7 @@ export const getPaymentFeeType = null, createAction(RECEIVE_PAYMENT_FEE_TYPE), `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}/fee-types/${paymentFeeTypeId}`, - authErrorHandler + snackbarErrorHandler )(params)(dispatch).then(() => { dispatch(stopLoading()); }); diff --git a/src/components/forms/payment-fee-type-form.js b/src/components/forms/payment-fee-type-form.js deleted file mode 100644 index 544987e5d..000000000 --- a/src/components/forms/payment-fee-type-form.js +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Copyright 2020 OpenStack Foundation - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * */ - -import React from "react"; -import T from "i18n-react/dist/i18n-react"; -import Input from "openstack-uicore-foundation/lib/components/inputs/text-input" -import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown"; -import { isEmpty, scrollToError, shallowEqual } from "../../utils/methods"; -import { MILLISECONDS_IN_SECOND } from "../../utils/constants"; - -class PaymentFeeTypeForm extends React.Component { - constructor(props) { - super(props); - - this.state = { - entity: { ...props.entity }, - errors: props.errors - }; - - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - } - - componentDidUpdate(prevProps) { - const state = {}; - scrollToError(this.props.errors); - - if (!shallowEqual(prevProps.entity, this.props.entity)) { - state.entity = { ...this.props.entity }; - state.errors = {}; - } - - if (!shallowEqual(prevProps.errors, this.props.errors)) { - state.errors = { ...this.props.errors }; - } - - if (!isEmpty(state)) { - this.setState({ ...this.state, ...state }); - } - } - - handleChange(ev) { - const entity = { ...this.state.entity }; - const errors = { ...this.state.errors }; - let { value, id } = ev.target; - - if (ev.target.type === "checkbox") { - value = ev.target.checked; - } - - if (ev.target.type === "datetime") { - value = value.valueOf() / MILLISECONDS_IN_SECOND; - } - - errors[id] = ""; - entity[id] = value; - this.setState({ entity, errors }); - } - - handleSubmit(ev) { - ev.preventDefault(); - this.props.onSubmit(this.state.entity); - } - - hasErrors(field) { - const { errors } = this.state; - if (field in errors) { - return errors[field]; - } - - return ""; - } - - render() { - const { entity } = this.state; - - const payment_type_fee_kind_ddl = [ - { label: "Rate", value: "Rate" }, - { label: "Amount", value: "Amount" } - ]; - - const payment_type_fee_method = [ - // # Cards & wallets - { label: "Card", value: "card" }, - { label: "Link", value: "link" }, - { label: "CashApp", value: "cashapp" }, - { label: "Paypal", value: "paypal" }, - // # Bank debits - { label: "UsBankAccount", value: "us_bank_account" }, - { label: "SepaDebit", value: "sepa_debit" }, - { label: "BacsDebit", value: "bacs_debit" }, - { label: "AuBecsDebit", value: "au_becs_debit" }, - { label: "AcssDebit", value: "acss_debit" }, - // # Bank redirects - { label: "Ideal", value: "ideal" }, - { label: "Sofort", value: "sofort" }, - { label: "Bancontact", value: "bancontact" }, - { label: "Giropay", value: "giropay" }, - { label: "Eps", value: "eps" }, - { label: "P24", value: "p24" }, - { label: "Blik", value: "blik" }, - // # Buy Now, Pay Later - { label: "Klarna", value: "klarna" }, - { label: "AfterpayClearpay", value: "afterpay_clearpay" }, - { label: "Affirm", value: "affirm" }, - // # Regional / other - { label: "Alipay", value: "alipay" }, - { label: "WechatPay", value: "wechat_pay" }, - { label: "Grabpay", value: "grabpay" }, - { label: "Oxxo", value: "oxxo" }, - { label: "Boleto", value: "boleto" }, - { label: "Konbini", value: "konbini" } - ]; - - return ( -
- -
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
-
- -
-
-
- ); - } -} - -export default PaymentFeeTypeForm; diff --git a/src/components/forms/payment-profile-form.js b/src/components/forms/payment-profile-form.js deleted file mode 100644 index 7ed1631fb..000000000 --- a/src/components/forms/payment-profile-form.js +++ /dev/null @@ -1,386 +0,0 @@ -/** - * Copyright 2020 OpenStack Foundation - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * */ - -import React from "react"; -import T from "i18n-react/dist/i18n-react"; -import Input from "openstack-uicore-foundation/lib/components/inputs/text-input" -import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown" -import Panel from "openstack-uicore-foundation/lib/components/sections/panel" -import Table from "openstack-uicore-foundation/lib/components/table"; -import { isEmpty, scrollToError, shallowEqual } from "../../utils/methods"; -import { MILLISECONDS_IN_SECOND } from "../../utils/constants"; - -class PaymentProfileForm extends React.Component { - constructor(props) { - super(props); - - this.state = { - entity: { ...props.entity }, - errors: props.errors - }; - - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleNewFeeType = this.handleNewFeeType.bind(this); - this.handleEditFeeType = this.handleEditFeeType.bind(this); - this.handleDeleteFeeType = this.handleDeleteFeeType.bind(this); - } - - componentDidUpdate(prevProps) { - const state = {}; - scrollToError(this.props.errors); - - if (!shallowEqual(prevProps.entity, this.props.entity)) { - state.entity = { ...this.props.entity }; - state.errors = {}; - } - - if (!shallowEqual(prevProps.errors, this.props.errors)) { - state.errors = { ...this.props.errors }; - } - - if (!isEmpty(state)) { - this.setState({ ...this.state, ...state }); - } - } - - handleChange(ev) { - const entity = { ...this.state.entity }; - const errors = { ...this.state.errors }; - let { value, id } = ev.target; - - if (ev.target.type === "checkbox") { - value = ev.target.checked; - } - - if (ev.target.type === "datetime") { - value = value.valueOf() / MILLISECONDS_IN_SECOND; - } - - errors[id] = ""; - entity[id] = value; - this.setState({ entity, errors }); - } - - handleSubmit(ev) { - ev.preventDefault(); - this.props.onSubmit(this.state.entity); - } - - hasErrors(field) { - const { errors } = this.state; - if (field in errors) { - return errors[field]; - } - - return ""; - } - - handleNewFeeType() { - const { currentSummit, entity, history } = this.props; - history.push( - `/app/summits/${currentSummit.id}/payment-profiles/${entity.id}/payment-fee-type/new` - ); - } - - handleEditFeeType(valueId) { - const { currentSummit, entity, history } = this.props; - history.push( - `/app/summits/${currentSummit.id}/payment-profiles/${entity.id}/payment-fee-type/${valueId}` - ); - } - - handleDeleteFeeType(valueId) { - this.props.onDeleteFeeType(valueId); - } - - render() { - const { entity } = this.state; - const { paymentFeeTypes } = this.props; - const application_type_ddl = [ - { label: "Registration", value: "Registration" }, - { label: "Bookable Rooms", value: "BookableRooms" }, - { label: "Sponsor Services", value: "SponsorServices" } - ]; - - const provider_ddl = [ - { label: "Stripe", value: "Stripe" }, - { label: "LawPay", value: "LawPay" } - ]; - - const fee_types_options = { - sortCol: paymentFeeTypes.order, - sortDir: paymentFeeTypes.orderDir, - actions: { - edit: { onClick: this.handleEditFeeType }, - delete: { onClick: this.handleDeleteFeeType } - } - }; - - const fee_types_columns = [ - { - columnKey: "name", - value: T.translate("edit_payment_profile.payment_type_fee_name") - }, - { - columnKey: "kind", - value: T.translate("edit_payment_profile.payment_type_fee_kind") - }, - { - columnKey: "payment_method", - value: T.translate("edit_payment_profile.payment_type_fee_method") - }, - { - columnKey: "value", - value: T.translate("edit_payment_profile.payment_type_fee_value") - }, - { - columnKey: "max_cents", - value: T.translate("edit_payment_profile.payment_type_fee_max_cents") - }, - { - columnKey: "min_cents", - value: T.translate("edit_payment_profile.payment_type_fee_min_cents") - } - ]; - - return ( -
- -
-
- - -
-
- - -
-
-
-
-
- - -
-
-
-
- - -   -