mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-19 13:54:25 +01:00
update the card for payment schedules
This commit is contained in:
parent
89adcf1a9d
commit
68f64cfc5c
@ -1,6 +1,9 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
## Next release
|
||||
- Ability to use PayZen a the payment gateway
|
||||
- For payment schedules, ability to update the related payment card before the deadline
|
||||
- Refactored data architecture to a more generic shopping cart model
|
||||
- Redesigned the data structure to allow buying multiple and various objects
|
||||
- Updated React and its dependencies to 17.0.3 and matching
|
||||
- Updated the dependencies of: webpack, lodash, eslint, webpack-dev-server, react2angular, auto-ngtemplate-loader, angular-bootstrap-switch, react-refresh-webpack-plugin and eslint-plugin-react
|
||||
|
@ -4,6 +4,7 @@
|
||||
class API::PayzenController < API::PaymentsController
|
||||
require 'pay_zen/charge'
|
||||
require 'pay_zen/order'
|
||||
require 'pay_zen/token'
|
||||
require 'pay_zen/transaction'
|
||||
require 'pay_zen/helper'
|
||||
|
||||
@ -27,7 +28,8 @@ class API::PayzenController < API::PaymentsController
|
||||
@result = client.create_payment(amount: amount[:amount],
|
||||
order_id: @id,
|
||||
customer: PayZen::Helper.generate_customer(params[:customer_id], current_user.id, params[:cart_items]))
|
||||
error_handling
|
||||
rescue PayzenError => e
|
||||
render json: e, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def create_token
|
||||
@ -35,7 +37,19 @@ class API::PayzenController < API::PaymentsController
|
||||
client = PayZen::Charge.new
|
||||
@result = client.create_token(order_id: @id,
|
||||
customer: PayZen::Helper.generate_customer(params[:customer_id], current_user.id, params[:cart_items]))
|
||||
error_handling
|
||||
rescue PayzenError => e
|
||||
render json: e, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def update_token
|
||||
schedule = PaymentSchedule.find(params[:payment_schedule_id])
|
||||
token = schedule.gateway_payment_mean
|
||||
@id = schedule.gateway_order.id
|
||||
@result = PayZen::Token.new.update(token.id,
|
||||
PayZen::Helper.generate_customer(schedule.user.id, current_user.id, schedule.to_cart),
|
||||
order_id: @id)
|
||||
rescue PayzenError => e
|
||||
render json: e, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def check_hash
|
||||
@ -81,10 +95,4 @@ class API::PayzenController < API::PaymentsController
|
||||
def on_payment_success(order_id, cart)
|
||||
super(order_id, 'PayZen::Order', cart)
|
||||
end
|
||||
|
||||
def error_handling
|
||||
return unless @result['status'] == 'ERROR'
|
||||
|
||||
render json: { error: @result['answer']['detailedErrorMessage'] || @result['answer']['errorMessage'] }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
@ -5,6 +5,8 @@ class API::StripeController < API::PaymentsController
|
||||
require 'stripe/helper'
|
||||
require 'stripe/service'
|
||||
|
||||
before_action :check_keys
|
||||
|
||||
##
|
||||
# Client requests to confirm a card payment will ask this endpoint.
|
||||
# It will check for the need of a strong customer authentication (SCA) to confirm the payment or confirm that the payment
|
||||
@ -86,6 +88,11 @@ class API::StripeController < API::PaymentsController
|
||||
Stripe::Customer.update(user.payment_gateway_object.gateway_object_id,
|
||||
{ invoice_settings: { default_payment_method: params[:payment_method_id] } },
|
||||
{ api_key: key })
|
||||
if params[:payment_schedule_id]
|
||||
schedule = PaymentSchedule.find(params[:payment_schedule_id])
|
||||
subscription = schedule.gateway_subscription.retrieve
|
||||
Stripe::Subscription.update(subscription.id, { default_payment_method: params[:payment_method_id] }, { api_key: key })
|
||||
end
|
||||
render json: { updated: true }, status: :ok
|
||||
rescue Stripe::StripeError => e
|
||||
render json: { updated: false, error: e }, status: :unprocessable_entity
|
||||
@ -128,4 +135,9 @@ class API::StripeController < API::PaymentsController
|
||||
{ status: 500, json: { error: 'Invalid PaymentIntent status' } }
|
||||
end
|
||||
end
|
||||
|
||||
def check_keys
|
||||
key = Setting.get('stripe_secret_key')
|
||||
raise Stripe::StripeError, 'Using live keys in development mode' if key&.match(/^sk_live_/) && Rails.env.development?
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +1,6 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { ShoppingCart } from '../models/payment';
|
||||
import { ShoppingCart, UpdateCardResponse } from '../models/payment';
|
||||
import { User } from '../models/user';
|
||||
import {
|
||||
CheckHashResponse,
|
||||
@ -42,4 +42,9 @@ export default class PayzenAPI {
|
||||
const res: AxiosResponse<PaymentSchedule> = await apiClient.post('/api/payzen/confirm_payment_schedule', { cart_items: cart, order_id: orderId, transaction_uuid: transactionUuid });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async updateToken(payment_schedule_id: number): Promise<CreateTokenResponse> {
|
||||
const res: AxiosResponse<CreateTokenResponse> = await apiClient.post(`/api/payzen/update_token`, { payment_schedule_id });
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
@ -26,10 +26,11 @@ export default class StripeAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async updateCard (user_id: number, stp_payment_method_id: string): Promise<UpdateCardResponse> {
|
||||
static async updateCard (user_id: number, stp_payment_method_id: string, payment_schedule_id?: number): Promise<UpdateCardResponse> {
|
||||
const res: AxiosResponse<UpdateCardResponse> = await apiClient.post(`/api/stripe/update_card`, {
|
||||
user_id,
|
||||
payment_method_id: stp_payment_method_id,
|
||||
payment_schedule_id
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
|
@ -12,7 +12,9 @@ import PaymentScheduleAPI from '../../api/payment-schedule';
|
||||
declare var Application: IApplication;
|
||||
|
||||
interface PaymentSchedulesDashboardProps {
|
||||
currentUser: User
|
||||
currentUser: User,
|
||||
onError: (message: string) => void,
|
||||
onCardUpdateSuccess: (message: string) => void,
|
||||
}
|
||||
|
||||
// how many payment schedules should we display for each page?
|
||||
@ -22,7 +24,7 @@ const PAGE_SIZE = 20;
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
* for the currentUser
|
||||
*/
|
||||
const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ currentUser }) => {
|
||||
const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
|
||||
const { t } = useTranslation('logged');
|
||||
|
||||
// list of displayed payment schedules
|
||||
@ -47,21 +49,28 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
|
||||
api.index({ query: { page: pageNumber + 1, size: PAGE_SIZE }}).then((res) => {
|
||||
const list = paymentSchedules.concat(res);
|
||||
setPaymentSchedules(list);
|
||||
});
|
||||
}).catch((error) => onError(error.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload from te API all the currently displayed payment schedules
|
||||
*/
|
||||
const handleRefreshList = (onError?: (msg: any) => void): void => {
|
||||
const handleRefreshList = (): void => {
|
||||
const api = new PaymentScheduleAPI();
|
||||
api.index({ query: { page: 1, size: PAGE_SIZE * pageNumber }}).then((res) => {
|
||||
setPaymentSchedules(res);
|
||||
}).catch((err) => {
|
||||
if (typeof onError === 'function') { onError(err.message); }
|
||||
onError(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* after a successful card update, provide a success message to the end-user
|
||||
*/
|
||||
const handleCardUpdateSuccess = (): void => {
|
||||
onCardUpdateSuccess(t('app.logged.dashboard.payment_schedules.card_updated_success'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current collection of payment schedules is empty or not.
|
||||
*/
|
||||
@ -80,7 +89,12 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
|
||||
<div className="payment-schedules-dashboard">
|
||||
{!hasSchedules() && <div>{t('app.logged.dashboard.payment_schedules.no_payment_schedules')}</div>}
|
||||
{hasSchedules() && <div className="schedules-list">
|
||||
<PaymentSchedulesTable paymentSchedules={paymentSchedules} showCustomer={false} refreshList={handleRefreshList} operator={currentUser} />
|
||||
<PaymentSchedulesTable paymentSchedules={paymentSchedules}
|
||||
showCustomer={false}
|
||||
refreshList={handleRefreshList}
|
||||
operator={currentUser}
|
||||
onError={onError}
|
||||
onCardUpdateSuccess={handleCardUpdateSuccess} />
|
||||
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.logged.dashboard.payment_schedules.load_more')}</FabButton>}
|
||||
</div>}
|
||||
</div>
|
||||
@ -88,12 +102,12 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
|
||||
}
|
||||
|
||||
|
||||
const PaymentSchedulesDashboardWrapper: React.FC<PaymentSchedulesDashboardProps> = ({ currentUser }) => {
|
||||
const PaymentSchedulesDashboardWrapper: React.FC<PaymentSchedulesDashboardProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PaymentSchedulesDashboard currentUser={currentUser} />
|
||||
<PaymentSchedulesDashboard currentUser={currentUser} onError={onError} onCardUpdateSuccess={onCardUpdateSuccess} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('paymentSchedulesDashboard', react2angular(PaymentSchedulesDashboardWrapper, ['currentUser']));
|
||||
Application.Components.component('paymentSchedulesDashboard', react2angular(PaymentSchedulesDashboardWrapper, ['currentUser', 'onError', 'onCardUpdateSuccess']));
|
||||
|
@ -13,7 +13,9 @@ import PaymentScheduleAPI from '../../api/payment-schedule';
|
||||
declare var Application: IApplication;
|
||||
|
||||
interface PaymentSchedulesListProps {
|
||||
currentUser: User
|
||||
currentUser: User,
|
||||
onError: (message: string) => void,
|
||||
onCardUpdateSuccess: (message: string) => void,
|
||||
}
|
||||
|
||||
// how many payment schedules should we display for each page?
|
||||
@ -22,7 +24,7 @@ const PAGE_SIZE = 20;
|
||||
/**
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
*/
|
||||
const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser }) => {
|
||||
const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// list of displayed payment schedules
|
||||
@ -54,7 +56,7 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
|
||||
const api = new PaymentScheduleAPI();
|
||||
api.list({ query: { reference, customer, date, page: 1, size: PAGE_SIZE }}).then((res) => {
|
||||
setPaymentSchedules(res);
|
||||
});
|
||||
}).catch((error) => onError(error.message));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -67,18 +69,18 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
|
||||
api.list({ query: { reference: referenceFilter, customer: customerFilter, date: dateFilter, page: pageNumber + 1, size: PAGE_SIZE }}).then((res) => {
|
||||
const list = paymentSchedules.concat(res);
|
||||
setPaymentSchedules(list);
|
||||
});
|
||||
}).catch((error) => onError(error.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload from te API all the currently displayed payment schedules
|
||||
*/
|
||||
const handleRefreshList = (onError?: (msg: any) => void): void => {
|
||||
const handleRefreshList = (): void => {
|
||||
const api = new PaymentScheduleAPI();
|
||||
api.list({ query: { reference: referenceFilter, customer: customerFilter, date: dateFilter, page: 1, size: PAGE_SIZE * pageNumber }}).then((res) => {
|
||||
setPaymentSchedules(res);
|
||||
}).catch((err) => {
|
||||
if (typeof onError === 'function') { onError(err.message); }
|
||||
onError(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
@ -96,6 +98,13 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
|
||||
return hasSchedules() && paymentSchedules.length < paymentSchedules[0].max_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* after a successful card update, provide a success message to the operator
|
||||
*/
|
||||
const handleCardUpdateSuccess = (): void => {
|
||||
onCardUpdateSuccess(t('app.admin.invoices.payment_schedules.card_updated_success'));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="payment-schedules-list">
|
||||
<h3>
|
||||
@ -107,7 +116,12 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
|
||||
</div>
|
||||
{!hasSchedules() && <div>{t('app.admin.invoices.payment_schedules.no_payment_schedules')}</div>}
|
||||
{hasSchedules() && <div className="schedules-list">
|
||||
<PaymentSchedulesTable paymentSchedules={paymentSchedules} showCustomer={true} refreshList={handleRefreshList} operator={currentUser} />
|
||||
<PaymentSchedulesTable paymentSchedules={paymentSchedules}
|
||||
showCustomer={true}
|
||||
refreshList={handleRefreshList}
|
||||
operator={currentUser}
|
||||
onError={onError}
|
||||
onCardUpdateSuccess={handleCardUpdateSuccess} />
|
||||
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.admin.invoices.payment_schedules.load_more')}</FabButton>}
|
||||
</div>}
|
||||
</div>
|
||||
@ -115,12 +129,12 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
|
||||
}
|
||||
|
||||
|
||||
const PaymentSchedulesListWrapper: React.FC<PaymentSchedulesListProps> = ({ currentUser }) => {
|
||||
const PaymentSchedulesListWrapper: React.FC<PaymentSchedulesListProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PaymentSchedulesList currentUser={currentUser} />
|
||||
<PaymentSchedulesList currentUser={currentUser} onError={onError} onCardUpdateSuccess={onCardUpdateSuccess} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('paymentSchedulesList', react2angular(PaymentSchedulesListWrapper, ['currentUser']));
|
||||
Application.Components.component('paymentSchedulesList', react2angular(PaymentSchedulesListWrapper, ['currentUser', 'onError', 'onCardUpdateSuccess']));
|
||||
|
@ -5,31 +5,31 @@ import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { UpdateCardModal } from '../payment/update-card-modal';
|
||||
import { StripeElements } from '../payment/stripe/stripe-elements';
|
||||
import { StripeConfirm } from '../payment/stripe/stripe-confirm';
|
||||
import { StripeCardUpdate } from '../payment/stripe/stripe-card-update';
|
||||
import { User, UserRole } from '../../models/user';
|
||||
import { IFablab } from '../../models/fablab';
|
||||
import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '../../models/payment-schedule';
|
||||
import PaymentScheduleAPI from '../../api/payment-schedule';
|
||||
|
||||
import stripeLogo from '../../../../images/powered_by_stripe.png';
|
||||
import mastercardLogo from '../../../../images/mastercard.png';
|
||||
import visaLogo from '../../../../images/visa.png';
|
||||
import { useImmer } from 'use-immer';
|
||||
import { SettingName } from '../../models/setting';
|
||||
|
||||
declare var Fablab: IFablab;
|
||||
|
||||
interface PaymentSchedulesTableProps {
|
||||
paymentSchedules: Array<PaymentSchedule>,
|
||||
showCustomer?: boolean,
|
||||
refreshList: (onError: (msg: any) => void) => void,
|
||||
refreshList: () => void,
|
||||
operator: User,
|
||||
onError: (message: string) => void,
|
||||
onCardUpdateSuccess: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
*/
|
||||
const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator }) => {
|
||||
const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator, onError, onCardUpdateSuccess }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
// for each payment schedule: are the details (all deadlines) shown or hidden?
|
||||
@ -46,13 +46,12 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
const [tempDeadline, setTempDeadline] = useState<PaymentScheduleItem>(null);
|
||||
// when an action is triggered on a deadline, the parent schedule is saved here until the action is done or cancelled.
|
||||
const [tempSchedule, setTempSchedule] = useState<PaymentSchedule>(null);
|
||||
// prevent submitting the form to update the card details, until all required fields are filled correctly
|
||||
const [canSubmitUpdateCard, setCanSubmitUpdateCard] = useState<boolean>(true);
|
||||
// errors are saved here, if any, for display purposes.
|
||||
const [errors, setErrors] = useState<string>(null);
|
||||
// is open, the modal dialog to cancel the associated subscription?
|
||||
const [showCancelSubscription, setShowCancelSubscription] = useState<boolean>(false);
|
||||
|
||||
// we want to display the card update button, only once. This is an association table keeping when we already shown one
|
||||
const cardUpdateButton = new Map<number, boolean>();
|
||||
|
||||
/**
|
||||
* Check if the requested payment schedule is displayed with its deadlines (PaymentScheduleItem) or without them
|
||||
*/
|
||||
@ -175,12 +174,14 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
);
|
||||
case PaymentScheduleItemState.RequirePaymentMethod:
|
||||
return (
|
||||
<FabButton onClick={handleUpdateCard(item, schedule)}
|
||||
<FabButton onClick={handleUpdateCard(schedule, item)}
|
||||
icon={<i className="fas fa-credit-card" />}>
|
||||
{t('app.shared.schedules_table.update_card')}
|
||||
</FabButton>
|
||||
);
|
||||
case PaymentScheduleItemState.Error:
|
||||
// if the payment is in error, the schedule is over, and we can't update the card
|
||||
cardUpdateButton.set(schedule.id, true);
|
||||
if (isPrivileged()) {
|
||||
return (
|
||||
<FabButton onClick={handleCancelSubscription(schedule)}
|
||||
@ -191,6 +192,17 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
} else {
|
||||
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>
|
||||
}
|
||||
case PaymentScheduleItemState.New:
|
||||
if (!cardUpdateButton.get(schedule.id)) {
|
||||
cardUpdateButton.set(schedule.id, true);
|
||||
return (
|
||||
<FabButton onClick={handleUpdateCard(schedule)}
|
||||
icon={<i className="fas fa-credit-card" />}>
|
||||
{t('app.shared.schedules_table.update_card')}
|
||||
</FabButton>
|
||||
)
|
||||
}
|
||||
return <span />
|
||||
default:
|
||||
return <span />
|
||||
}
|
||||
@ -223,7 +235,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
* Refresh all payment schedules in the table
|
||||
*/
|
||||
const refreshSchedulesTable = (): void => {
|
||||
refreshList(setErrors);
|
||||
refreshList();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -272,7 +284,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
/**
|
||||
* Callback triggered when the user's clicks on the "update card" button: show a modal to input a new card
|
||||
*/
|
||||
const handleUpdateCard = (item: PaymentScheduleItem, paymentSchedule: PaymentSchedule): ReactEventHandler => {
|
||||
const handleUpdateCard = (paymentSchedule: PaymentSchedule, item?: PaymentScheduleItem): ReactEventHandler => {
|
||||
return (): void => {
|
||||
setTempDeadline(item);
|
||||
setTempSchedule(paymentSchedule);
|
||||
@ -287,46 +299,31 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
setShowUpdateCard(!showUpdateCard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the logos, shown in the modal footer.
|
||||
*/
|
||||
const logoFooter = (): ReactNode => {
|
||||
return (
|
||||
<div className="stripe-modal-icons">
|
||||
<i className="fa fa-lock fa-2x m-r-sm pos-rlt" />
|
||||
<img src={stripeLogo} alt="powered by stripe" />
|
||||
<img src={mastercardLogo} alt="mastercard" />
|
||||
<img src={visaLogo} alt="visa" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the submit button is pushed, disable it to prevent double form submission
|
||||
*/
|
||||
const handleCardUpdateSubmit = (): void => {
|
||||
setCanSubmitUpdateCard(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the card was successfully updated, pay the invoice (using the new payment method) and close the modal
|
||||
*/
|
||||
const handleCardUpdateSuccess = (): void => {
|
||||
const api = new PaymentScheduleAPI();
|
||||
api.payItem(tempDeadline.id).then(() => {
|
||||
refreshSchedulesTable();
|
||||
if (tempDeadline) {
|
||||
const api = new PaymentScheduleAPI();
|
||||
api.payItem(tempDeadline.id).then(() => {
|
||||
refreshSchedulesTable();
|
||||
onCardUpdateSuccess();
|
||||
toggleUpdateCardModal();
|
||||
}).catch((err) => {
|
||||
handleCardUpdateError(err);
|
||||
});
|
||||
} else {
|
||||
// if no tempDeadline (i.e. PaymentScheduleItem), then the user is updating his card number in a pro-active way, we don't need to trigger the payment
|
||||
onCardUpdateSuccess();
|
||||
toggleUpdateCardModal();
|
||||
}).catch((err) => {
|
||||
handleCardUpdateError(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the card was not updated, show the error
|
||||
* When the card was not updated, raise the error
|
||||
*/
|
||||
const handleCardUpdateError = (error): void => {
|
||||
setErrors(error);
|
||||
setCanSubmitUpdateCard(true);
|
||||
onError(error);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -445,31 +442,13 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
preventConfirm={isConfirmActionDisabled}>
|
||||
{tempDeadline && <StripeConfirm clientSecret={tempDeadline.client_secret} onResponse={toggleConfirmActionButton} />}
|
||||
</FabModal>
|
||||
<FabModal title={t('app.shared.schedules_table.update_card')}
|
||||
isOpen={showUpdateCard}
|
||||
toggleModal={toggleUpdateCardModal}
|
||||
closeButton={false}
|
||||
customFooter={logoFooter()}
|
||||
className="update-card-modal">
|
||||
{tempDeadline && tempSchedule && <StripeCardUpdate onSubmit={handleCardUpdateSubmit}
|
||||
onSuccess={handleCardUpdateSuccess}
|
||||
onError={handleCardUpdateError}
|
||||
customerId={tempSchedule.user.id}
|
||||
operator={operator}
|
||||
className="card-form" >
|
||||
{errors && <div className="stripe-errors">
|
||||
{errors}
|
||||
</div>}
|
||||
</StripeCardUpdate>}
|
||||
<div className="submit-card">
|
||||
{canSubmitUpdateCard && <button type="submit" disabled={!canSubmitUpdateCard} form="stripe-card" className="submit-card-btn">{t('app.shared.schedules_table.validate_button')}</button>}
|
||||
{!canSubmitUpdateCard && <div className="payment-pending">
|
||||
<div className="fa-2x">
|
||||
<i className="fas fa-circle-notch fa-spin" />
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</FabModal>
|
||||
{tempSchedule && <UpdateCardModal isOpen={showUpdateCard}
|
||||
toggleModal={toggleUpdateCardModal}
|
||||
operator={operator}
|
||||
afterSuccess={handleCardUpdateSuccess}
|
||||
onError={handleCardUpdateError}
|
||||
schedule={tempSchedule}>
|
||||
</UpdateCardModal>}
|
||||
</StripeElements>
|
||||
</div>
|
||||
</div>
|
||||
@ -478,10 +457,10 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
PaymentSchedulesTableComponent.defaultProps = { showCustomer: false };
|
||||
|
||||
|
||||
export const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator }) => {
|
||||
export const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator, onError, onCardUpdateSuccess }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PaymentSchedulesTableComponent paymentSchedules={paymentSchedules} showCustomer={showCustomer} refreshList={refreshList} operator={operator} />
|
||||
<PaymentSchedulesTableComponent paymentSchedules={paymentSchedules} showCustomer={showCustomer} refreshList={refreshList} operator={operator} onError={onError} onCardUpdateSuccess={onCardUpdateSuccess} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,90 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabModal } from '../../base/fab-modal';
|
||||
import { PaymentSchedule } from '../../../models/payment-schedule';
|
||||
import { User } from '../../../models/user';
|
||||
import mastercardLogo from '../../../../../images/mastercard.png';
|
||||
import visaLogo from '../../../../../images/visa.png';
|
||||
import payzenLogo from '../../../../../images/payzen-secure.png';
|
||||
import { PayzenForm } from './payzen-form';
|
||||
|
||||
interface PayzenCardUpdateModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
onSuccess: () => void,
|
||||
schedule: PaymentSchedule,
|
||||
operator: User
|
||||
}
|
||||
|
||||
export const PayzenCardUpdateModal: React.FC<PayzenCardUpdateModalProps> = ({ isOpen, toggleModal, onSuccess, schedule, operator }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
// prevent submitting the form to update the card details, until the user has filled correctly all required fields
|
||||
const [canSubmitUpdateCard, setCanSubmitUpdateCard] = useState<boolean>(true);
|
||||
// we save errors here, if any, for display purposes.
|
||||
const [errors, setErrors] = useState<string>(null);
|
||||
|
||||
// the unique identifier of the html form
|
||||
const formId = "payzen-card";
|
||||
|
||||
/**
|
||||
* Return the logos, shown in the modal footer.
|
||||
*/
|
||||
const logoFooter = (): ReactNode => {
|
||||
return (
|
||||
<div className="payzen-modal-icons">
|
||||
<img src={payzenLogo} alt="powered by PayZen" />
|
||||
<img src={mastercardLogo} alt="mastercard" />
|
||||
<img src={visaLogo} alt="visa" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* When the user clicks the submit button, we disable it to prevent double form submission
|
||||
*/
|
||||
const handleCardUpdateSubmit = (): void => {
|
||||
setCanSubmitUpdateCard(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the card was not updated, show the error
|
||||
*/
|
||||
const handleCardUpdateError = (error): void => {
|
||||
setErrors(error);
|
||||
setCanSubmitUpdateCard(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<FabModal title={t('app.shared.payzen_card_update_modal.update_card')}
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
closeButton={false}
|
||||
customFooter={logoFooter()}
|
||||
className="payzen-update-card-modal">
|
||||
{schedule && <PayzenForm onSubmit={handleCardUpdateSubmit}
|
||||
onSuccess={onSuccess}
|
||||
onError={handleCardUpdateError}
|
||||
className="card-form"
|
||||
paymentSchedule={true}
|
||||
operator={operator}
|
||||
customer={schedule.user as User}
|
||||
updateCard={true}
|
||||
paymentScheduleId={schedule.id}
|
||||
formId={formId} >
|
||||
{errors && <div className="payzen-errors">
|
||||
{errors}
|
||||
</div>}
|
||||
</PayzenForm>}
|
||||
<div className="submit-card">
|
||||
{canSubmitUpdateCard && <button type="submit" disabled={!canSubmitUpdateCard} form={formId} className="submit-card-btn">{t('app.shared.payzen_card_update_modal.validate_button')}</button>}
|
||||
{!canSubmitUpdateCard && <div className="payment-pending">
|
||||
<div className="fa-2x">
|
||||
<i className="fas fa-circle-notch fa-spin" />
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</FabModal>
|
||||
);
|
||||
}
|
@ -14,11 +14,17 @@ import {
|
||||
import { PaymentSchedule } from '../../../models/payment-schedule';
|
||||
import { Invoice } from '../../../models/invoice';
|
||||
|
||||
// we use these two additional parameters to update the card, if provided
|
||||
interface PayzenFormProps extends GatewayFormProps {
|
||||
updateCard?: boolean,
|
||||
paymentScheduleId: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* A form component to collect the credit card details and to create the payment method on Stripe.
|
||||
* The form validation button must be created elsewhere, using the attribute form={formId}.
|
||||
*/
|
||||
export const PayzenForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cart, customer, operator, formId }) => {
|
||||
export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, updateCard = false, cart, customer, formId, paymentScheduleId }) => {
|
||||
|
||||
const PayZenKR = useRef<KryptonClient>(null);
|
||||
const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader');
|
||||
@ -48,7 +54,9 @@ export const PayzenForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
|
||||
* Depending on the current transaction (schedule or not), a PayZen Token or Payment may be created.
|
||||
*/
|
||||
const createToken = async (): Promise<CreateTokenResponse> => {
|
||||
if (paymentSchedule) {
|
||||
if (updateCard) {
|
||||
return await PayzenAPI.updateToken(paymentScheduleId);
|
||||
} else if (paymentSchedule) {
|
||||
return await PayzenAPI.chargeCreateToken(cart, customer);
|
||||
} else {
|
||||
return await PayzenAPI.chargeCreatePayment(cart, customer);
|
||||
@ -62,8 +70,9 @@ export const PayzenForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
|
||||
const onPaid = (event: ProcessPaymentAnswer): boolean => {
|
||||
PayzenAPI.checkHash(event.hashAlgorithm, event.hashKey, event.hash, event.rawClientAnswer).then(async (hash) => {
|
||||
if (hash.validity) {
|
||||
const transaction = event.clientAnswer.transactions[0];
|
||||
if (updateCard) return onSuccess(null);
|
||||
|
||||
const transaction = event.clientAnswer.transactions[0];
|
||||
if (event.clientAnswer.orderStatus === 'PAID') {
|
||||
confirmPayment(event, transaction).then((confirmation) => {
|
||||
PayZenKR.current.removeForms().then(() => {
|
||||
@ -81,13 +90,12 @@ export const PayzenForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
|
||||
|
||||
/**
|
||||
* Confirm the payment, depending on the current type of payment (single shot or recurring)
|
||||
* @param event
|
||||
*/
|
||||
const confirmPayment = async (event: ProcessPaymentAnswer, transaction: PaymentTransaction): Promise<Invoice|PaymentSchedule> => {
|
||||
if (!paymentSchedule) {
|
||||
return await PayzenAPI.confirm(event.clientAnswer.orderDetails.orderId, cart);
|
||||
} else {
|
||||
if (paymentSchedule) {
|
||||
return await PayzenAPI.confirmPaymentSchedule(event.clientAnswer.orderDetails.orderId, transaction.uuid, cart);
|
||||
} else {
|
||||
return await PayzenAPI.confirm(event.clientAnswer.orderDetails.orderId, cart);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,84 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabModal } from '../../base/fab-modal';
|
||||
import { StripeCardUpdate } from './stripe-card-update';
|
||||
import { PaymentSchedule } from '../../../models/payment-schedule';
|
||||
import { User } from '../../../models/user';
|
||||
import stripeLogo from '../../../../../images/powered_by_stripe.png';
|
||||
import mastercardLogo from '../../../../../images/mastercard.png';
|
||||
import visaLogo from '../../../../../images/visa.png';
|
||||
|
||||
interface StripeCardUpdateModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
onSuccess: () => void,
|
||||
schedule: PaymentSchedule,
|
||||
operator: User
|
||||
}
|
||||
|
||||
export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ isOpen, toggleModal, onSuccess, schedule, operator }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
// prevent submitting the form to update the card details, until the user has filled correctly all required fields
|
||||
const [canSubmitUpdateCard, setCanSubmitUpdateCard] = useState<boolean>(true);
|
||||
// we save errors here, if any, for display purposes.
|
||||
const [errors, setErrors] = useState<string>(null);
|
||||
|
||||
/**
|
||||
* Return the logos, shown in the modal footer.
|
||||
*/
|
||||
const logoFooter = (): ReactNode => {
|
||||
return (
|
||||
<div className="stripe-modal-icons">
|
||||
<i className="fa fa-lock fa-2x m-r-sm pos-rlt" />
|
||||
<img src={stripeLogo} alt="powered by stripe" />
|
||||
<img src={mastercardLogo} alt="mastercard" />
|
||||
<img src={visaLogo} alt="visa" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* When the user clicks the submit button, we disable it to prevent double form submission
|
||||
*/
|
||||
const handleCardUpdateSubmit = (): void => {
|
||||
setCanSubmitUpdateCard(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the card was not updated, show the error
|
||||
*/
|
||||
const handleCardUpdateError = (error): void => {
|
||||
setErrors(error);
|
||||
setCanSubmitUpdateCard(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<FabModal title={t('app.shared.stripe_card_update_modal.update_card')}
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
closeButton={false}
|
||||
customFooter={logoFooter()}
|
||||
className="stripe-update-card-modal">
|
||||
{schedule && <StripeCardUpdate onSubmit={handleCardUpdateSubmit}
|
||||
onSuccess={onSuccess}
|
||||
onError={handleCardUpdateError}
|
||||
schedule={schedule}
|
||||
operator={operator}
|
||||
className="card-form" >
|
||||
{errors && <div className="stripe-errors">
|
||||
{errors}
|
||||
</div>}
|
||||
</StripeCardUpdate>}
|
||||
<div className="submit-card">
|
||||
{canSubmitUpdateCard && <button type="submit" disabled={!canSubmitUpdateCard} form="stripe-card" className="submit-card-btn">{t('app.shared.stripe_card_update_modal.validate_button')}</button>}
|
||||
{!canSubmitUpdateCard && <div className="payment-pending">
|
||||
<div className="fa-2x">
|
||||
<i className="fas fa-circle-notch fa-spin" />
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</FabModal>
|
||||
);
|
||||
}
|
@ -1,15 +1,14 @@
|
||||
import React, { FormEvent } from 'react';
|
||||
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
||||
import { SetupIntent } from "@stripe/stripe-js";
|
||||
import { PaymentConfirmation } from '../../../models/payment';
|
||||
import { User } from '../../../models/user';
|
||||
import StripeAPI from '../../../api/stripe';
|
||||
import { PaymentSchedule } from '../../../models/payment-schedule';
|
||||
|
||||
interface StripeCardUpdateProps {
|
||||
onSubmit: () => void,
|
||||
onSuccess: (result: SetupIntent|PaymentConfirmation|any) => void,
|
||||
onSuccess: () => void,
|
||||
onError: (message: string) => void,
|
||||
customerId: number,
|
||||
schedule: PaymentSchedule
|
||||
operator: User,
|
||||
className?: string,
|
||||
}
|
||||
@ -19,7 +18,7 @@ interface StripeCardUpdateProps {
|
||||
*
|
||||
* The form validation button must be created elsewhere, using the attribute form="stripe-card".
|
||||
*/
|
||||
export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, onSuccess, onError, className, customerId, operator, children }) => {
|
||||
export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, onSuccess, onError, className, schedule, operator, children }) => {
|
||||
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
@ -47,7 +46,7 @@ export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, on
|
||||
} else {
|
||||
try {
|
||||
// we start by associating the payment method with the user
|
||||
const { client_secret } = await StripeAPI.setupIntent(customerId);
|
||||
const { client_secret } = await StripeAPI.setupIntent(schedule.user.id);
|
||||
const { error } = await stripe.confirmCardSetup(client_secret, {
|
||||
payment_method: paymentMethod.id,
|
||||
mandate_data: {
|
||||
@ -64,8 +63,12 @@ export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, on
|
||||
onError(error.message);
|
||||
} else {
|
||||
// then we update the default payment method
|
||||
const res = await StripeAPI.updateCard(customerId, paymentMethod.id);
|
||||
onSuccess(res);
|
||||
const res = await StripeAPI.updateCard(schedule.user.id, paymentMethod.id, schedule.id);
|
||||
if (res.updated) {
|
||||
onSuccess();
|
||||
} else {
|
||||
onError(res.error);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// catch api errors
|
||||
|
@ -0,0 +1,83 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import { Loader } from '../base/loader';
|
||||
import { StripeCardUpdateModal } from './stripe/stripe-card-update-modal';
|
||||
import { PayzenCardUpdateModal } from './payzen/payzen-card-update-modal';
|
||||
import { User } from '../../models/user';
|
||||
import { PaymentSchedule } from '../../models/payment-schedule';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
interface UpdateCardModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
afterSuccess: () => void,
|
||||
onError: (message: string) => void,
|
||||
schedule: PaymentSchedule,
|
||||
operator: User
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This component open a modal dialog for the configured payment gateway, allowing the user to input his card data
|
||||
* to process an online payment.
|
||||
*/
|
||||
const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, operator, schedule }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
const [gateway, setGateway] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (schedule.gateway_subscription.classname.match(/^PayZen::/)) {
|
||||
setGateway('payzen');
|
||||
} else if (schedule.gateway_subscription.classname.match(/^Stripe::/)) {
|
||||
setGateway('stripe');
|
||||
}
|
||||
}, [schedule]);
|
||||
|
||||
/**
|
||||
* Render the Stripe update-card modal
|
||||
*/
|
||||
const renderStripeModal = (): ReactElement => {
|
||||
return <StripeCardUpdateModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
onSuccess={afterSuccess}
|
||||
operator={operator}
|
||||
schedule={schedule} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the PayZen update-card modal
|
||||
*/ // 1
|
||||
const renderPayZenModal = (): ReactElement => {
|
||||
return <PayzenCardUpdateModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
onSuccess={afterSuccess}
|
||||
operator={operator}
|
||||
schedule={schedule} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which gateway is in use with the current schedule and return the appropriate modal
|
||||
*/
|
||||
|
||||
switch (gateway) {
|
||||
case 'stripe':
|
||||
return renderStripeModal();
|
||||
case 'payzen':
|
||||
return renderPayZenModal();
|
||||
case '':
|
||||
return <div/>;
|
||||
default:
|
||||
onError(t('app.shared.update_card_modal.unexpected_error'));
|
||||
console.error(`[UpdateCardModal] unexpected gateway: ${schedule.gateway_subscription?.classname}`);
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const UpdateCardModal: React.FC<UpdateCardModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, operator, schedule }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<UpdateCardModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} operator={operator} schedule={schedule} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
@ -703,6 +703,20 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used in PaymentScheduleList, in case of error
|
||||
*/
|
||||
$scope.onError = function (message) {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user has successfully updated his card
|
||||
*/
|
||||
$scope.onCardUpdateSuccess = function (message) {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered after the gateway failed to be configured
|
||||
*/
|
||||
|
@ -62,6 +62,20 @@ Application.Controllers.controller('DashboardController', ['$scope', 'memberProm
|
||||
return networks;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used in PaymentScheduleDashboard, in case of error
|
||||
*/
|
||||
$scope.onError = function (message) {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user has successfully updated his card
|
||||
*/
|
||||
$scope.onCardUpdateSuccess = function (message) {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
// !!! MUST BE CALLED AT THE END of the controller
|
||||
return initialize();
|
||||
}
|
||||
|
@ -20,23 +20,15 @@ export interface PaymentScheduleItem {
|
||||
state: PaymentScheduleItemState,
|
||||
invoice_id: number,
|
||||
payment_method: PaymentMethod,
|
||||
client_secret?: string,
|
||||
details: {
|
||||
recurring: number,
|
||||
adjustment?: number,
|
||||
other_items?: number,
|
||||
without_coupon?: number
|
||||
}
|
||||
client_secret?: string
|
||||
}
|
||||
|
||||
export interface PaymentSchedule {
|
||||
max_length: number;
|
||||
id: number,
|
||||
total: number,
|
||||
stp_subscription_id: string,
|
||||
reference: string,
|
||||
payment_method: string,
|
||||
wallet_amount: number,
|
||||
items: Array<PaymentScheduleItem>,
|
||||
created_at: Date,
|
||||
chained_footprint: boolean,
|
||||
@ -52,6 +44,9 @@ export interface PaymentSchedule {
|
||||
id: number,
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
},
|
||||
gateway_subscription: {
|
||||
classname: string
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ export enum PaymentMethod {
|
||||
Other = ''
|
||||
}
|
||||
|
||||
export type CartItem = { reservation: Reservation }|{ subscription: SubscriptionRequest };
|
||||
export type CartItem = { reservation: Reservation }|{ subscription: SubscriptionRequest }|{ card_update: { date: Date } };
|
||||
|
||||
export interface ShoppingCart {
|
||||
customer_id: number,
|
||||
|
@ -41,5 +41,7 @@
|
||||
@import "modules/payzen-settings";
|
||||
@import "modules/payment-modal";
|
||||
@import "modules/payzen-modal";
|
||||
@import "modules/stripe-update-card-modal";
|
||||
@import "modules/payzen-update-card-modal";
|
||||
|
||||
@import "app.responsive";
|
||||
|
@ -121,61 +121,3 @@
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.fab-modal.update-card-modal {
|
||||
.fab-modal-content {
|
||||
.card-form {
|
||||
background-color: #f4f3f3;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px 6px 0 0;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
padding: 15px;
|
||||
|
||||
.stripe-errors {
|
||||
padding: 4px 0;
|
||||
color: #9e2146;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
.submit-card {
|
||||
.submit-card-btn {
|
||||
width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 0 6px 6px;
|
||||
border-top: 0;
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
background-color: #1d98ec;
|
||||
margin-bottom: 15px;
|
||||
|
||||
&[disabled] {
|
||||
background-color: lighten(#1d98ec, 20%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.payment-pending {
|
||||
@extend .submit-card-btn;
|
||||
background-color: lighten(#1d98ec, 20%);
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fab-modal-footer {
|
||||
.stripe-modal-icons {
|
||||
& {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fa.fa-lock {
|
||||
top: 7px;
|
||||
color: #9edd78;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
.payzen-update-card-modal {
|
||||
.card-form {
|
||||
background-color: #f4f3f3;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px 6px 0 0;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
padding: 15px;
|
||||
|
||||
.payzen-errors {
|
||||
padding: 4px 0;
|
||||
color: #9e2146;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.loader {
|
||||
text-align: center;
|
||||
}
|
||||
.loader-overlay {
|
||||
position: absolute;
|
||||
top: 65px;
|
||||
left: 190px;
|
||||
z-index: 1;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: inherit;
|
||||
|
||||
.kr-payment-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kr-form-error {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fab-modal-content {
|
||||
.submit-card {
|
||||
.submit-card-btn {
|
||||
width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 0 6px 6px;
|
||||
border-top: 0;
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
background-color: #1d98ec;
|
||||
margin-bottom: 15px;
|
||||
|
||||
&[disabled] {
|
||||
background-color: lighten(#1d98ec, 20%);
|
||||
}
|
||||
}
|
||||
|
||||
.payment-pending {
|
||||
@extend .submit-card-btn;
|
||||
background-color: lighten(#1d98ec, 20%);
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.payzen-modal-icons {
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
.stripe-update-card-modal {
|
||||
.fab-modal-content {
|
||||
.card-form {
|
||||
background-color: #f4f3f3;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px 6px 0 0;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
padding: 15px;
|
||||
|
||||
.stripe-errors {
|
||||
padding: 4px 0;
|
||||
color: #9e2146;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
.submit-card {
|
||||
.submit-card-btn {
|
||||
width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 0 6px 6px;
|
||||
border-top: 0;
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
background-color: #1d98ec;
|
||||
margin-bottom: 15px;
|
||||
|
||||
&[disabled] {
|
||||
background-color: lighten(#1d98ec, 20%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.payment-pending {
|
||||
@extend .submit-card-btn;
|
||||
background-color: lighten(#1d98ec, 20%);
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fab-modal-footer {
|
||||
.stripe-modal-icons {
|
||||
& {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fa.fa-lock {
|
||||
top: 7px;
|
||||
color: #9edd78;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab heading="{{ 'app.admin.invoices.payment_schedules_list' | translate }}" ng-show="$root.modules.invoicing" index="4" class="payment-schedules-list">
|
||||
<payment-schedules-list current-user="currentUser" />
|
||||
<payment-schedules-list current-user="currentUser" on-error="onError" on-card-update-success="onCardUpdateSuccess" />
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab heading="{{ 'app.admin.invoices.invoicing_settings' | translate }}" index="1" class="invoices-settings">
|
||||
@ -59,7 +59,7 @@
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab heading="{{ 'app.admin.invoices.payment_schedules_list' | translate }}" index="4" class="payment-schedules-list">
|
||||
<payment-schedules-list current-user="currentUser" />
|
||||
<payment-schedules-list current-user="currentUser" on-error="onError" on-card-update-success="onCardUpdateSuccess" />
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</div>
|
||||
|
@ -7,5 +7,5 @@
|
||||
|
||||
</section>
|
||||
|
||||
<payment-schedules-dashboard current-user="currentUser" />
|
||||
<payment-schedules-dashboard current-user="currentUser" on-error="onError" on-card-update-success="onCardUpdateSuccess" />
|
||||
</div>
|
||||
|
@ -53,7 +53,7 @@ class NotificationType
|
||||
notify_admin_refund_created
|
||||
notify_admins_role_update
|
||||
notify_user_role_update
|
||||
notify_admin_members_stripe_sync
|
||||
notify_admin_objects_stripe_sync
|
||||
notify_user_when_payment_schedule_ready
|
||||
notify_admin_payment_schedule_failed
|
||||
notify_member_payment_schedule_failed
|
||||
|
@ -50,11 +50,15 @@ class PaymentSchedule < PaymentDocument
|
||||
end
|
||||
|
||||
def gateway_subscription
|
||||
payment_gateway_objects.map(&:gateway_object).find { |item| !item.payment_mean? }
|
||||
payment_gateway_objects.map(&:gateway_object).find(&:subscription?)
|
||||
end
|
||||
|
||||
def gateway_order
|
||||
payment_gateway_objects.map(&:gateway_object).find(&:order?)
|
||||
end
|
||||
|
||||
def main_object
|
||||
payment_schedule_objects.where(main: true).first
|
||||
payment_schedule_objects.find_by(main: true)
|
||||
end
|
||||
|
||||
def user
|
||||
@ -81,6 +85,11 @@ class PaymentSchedule < PaymentDocument
|
||||
{ partial: 'api/payment_schedules/payment_schedule', locals: { payment_schedule: self } }
|
||||
end
|
||||
|
||||
def to_cart
|
||||
service = CartService.new(operator_profile.user)
|
||||
service.from_payment_schedule(self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_and_send_document
|
||||
|
@ -36,6 +36,32 @@ class CartService
|
||||
)
|
||||
end
|
||||
|
||||
def from_payment_schedule(payment_schedule)
|
||||
@customer = payment_schedule.user
|
||||
plan = payment_schedule.payment_schedule_objects.find(&:subscription)&.subscription&.plan
|
||||
|
||||
coupon = CartItem::Coupon.new(@customer, @operator, payment_schedule.coupon&.code)
|
||||
schedule = CartItem::PaymentSchedule.new(plan, coupon, true)
|
||||
|
||||
items = []
|
||||
payment_schedule.payment_schedule_objects.each do |object|
|
||||
if object.subscription
|
||||
items.push(CartItem::Subscription.new(object.subscription.plan, @customer))
|
||||
elsif object.reservation
|
||||
items.push(reservable_from_payment_schedule_object(object, plan))
|
||||
end
|
||||
end
|
||||
|
||||
ShoppingCart.new(
|
||||
@customer,
|
||||
@operator,
|
||||
coupon,
|
||||
schedule,
|
||||
payment_schedule.payment_method,
|
||||
items: items
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def plan(cart_items)
|
||||
@ -98,4 +124,41 @@ class CartService
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
|
||||
def reservable_from_payment_schedule_object(object, plan)
|
||||
reservable = object.reservation.reservable
|
||||
case reservable
|
||||
when Machine
|
||||
CartItem::MachineReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
object.reservation.slots,
|
||||
plan: plan,
|
||||
new_subscription: true)
|
||||
when Training
|
||||
CartItem::TrainingReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
object.reservation.slots,
|
||||
plan: plan,
|
||||
new_subscription: true)
|
||||
when Event
|
||||
CartItem::EventReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
object.reservation.slots,
|
||||
normal_tickets: object.reservation.nb_reserve_places,
|
||||
other_tickets: object.reservation.tickets)
|
||||
when Space
|
||||
CartItem::SpaceReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
object.reservation.slots,
|
||||
plan: plan,
|
||||
new_subscription: true)
|
||||
else
|
||||
STDERR.puts "WARNING: the reservable #{reservable} is not implemented"
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -48,7 +48,7 @@ class PaymentGatewayService
|
||||
private
|
||||
|
||||
def service_for_payment_schedule(payment_schedule)
|
||||
service = case payment_schedule_item.payment_schedule.gateway_subscription.klass
|
||||
service = case payment_schedule.gateway_subscription.klass
|
||||
when /^PayZen::/
|
||||
require 'pay_zen/service'
|
||||
PayZen::Service
|
||||
|
@ -18,8 +18,8 @@ class SettingService
|
||||
# notify about a change in privacy policy
|
||||
NotifyPrivacyUpdateWorker.perform_async(setting.id) if setting.name == 'privacy_body'
|
||||
|
||||
# sync all users on stripe
|
||||
SyncMembersOnStripeWorker.perform_async(setting.history_values.last&.invoicing_profile&.user&.id) if setting.name == 'stripe_secret_key'
|
||||
# sync all objects on stripe
|
||||
SyncObjectsOnStripeWorker.perform_async(setting.history_values.last&.invoicing_profile&.user&.id) if setting.name == 'stripe_secret_key'
|
||||
|
||||
# generate statistics
|
||||
PeriodStatisticsWorker.perform_async(setting.previous_update) if setting.name == 'statistics_module' && setting.value == 'true'
|
||||
|
@ -1,2 +1,2 @@
|
||||
json.title notification.notification_type
|
||||
json.description t('.all_members_sync')
|
||||
json.description t('.all_objects_sync')
|
@ -17,6 +17,12 @@ json.main_object do
|
||||
json.type payment_schedule.main_object.object_type
|
||||
json.id payment_schedule.main_object.object_id
|
||||
end
|
||||
if payment_schedule.gateway_subscription
|
||||
json.gateway_subscription do
|
||||
# this attribute is used to known which gateway should we interact with, in the front-end
|
||||
json.classname payment_schedule.gateway_subscription.klass
|
||||
end
|
||||
end
|
||||
json.items payment_schedule.payment_schedule_items do |item|
|
||||
json.extract! item, :id, :due_date, :state, :invoice_id, :payment_method
|
||||
json.amount item.amount / 100.00
|
||||
|
4
app/views/api/payzen/update_token.json.jbuilder
Normal file
4
app/views/api/payzen/update_token.json.jbuilder
Normal file
@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.formToken @result['answer']['formToken']
|
||||
json.orderId @id
|
@ -20,8 +20,7 @@
|
||||
<script type="text/javascript" src="//cdn.crowdin.com/jipt/jipt.js"></script>
|
||||
<% end %>
|
||||
|
||||
<% require 'pay_zen/helper' %>
|
||||
<% if PayZen::Helper.enabled? %>
|
||||
<% if Setting.get('payzen_endpoint') %>
|
||||
<% endpoint = Setting.get('payzen_endpoint') %>
|
||||
<%=stylesheet_link_tag "#{endpoint}/static/js/krypton-client/V4.0/ext/classic-reset.css" %>
|
||||
<%=javascript_include_tag "#{endpoint}/static/js/krypton-client/V4.0/ext/classic.js" %>
|
||||
|
@ -1 +0,0 @@
|
||||
<p><%= t('.body.members_sync') %></p>
|
@ -0,0 +1 @@
|
||||
<p><%= t('.body.objects_sync') %></p>
|
@ -21,7 +21,9 @@ class StripeWorker
|
||||
},
|
||||
{ api_key: Setting.get('stripe_secret_key') }
|
||||
)
|
||||
user.update_columns(stp_customer_id: customer.id)
|
||||
pgo = PaymentGatewayObject.find_or_initialize_by(item: user)
|
||||
pgo.gateway_object = customer
|
||||
pgo.save!
|
||||
end
|
||||
|
||||
def delete_stripe_coupon(coupon_code)
|
||||
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This worker perform various requests to the Stripe API (payment service)
|
||||
class SyncMembersOnStripeWorker
|
||||
class SyncObjectsOnStripeWorker
|
||||
include Sidekiq::Worker
|
||||
sidekiq_options lock: :until_executed, on_conflict: :reject, queue: :stripe
|
||||
|
||||
@ -17,12 +17,31 @@ class SyncMembersOnStripeWorker
|
||||
StripeWorker.new.create_stripe_customer(member.id)
|
||||
end
|
||||
end
|
||||
|
||||
logger.debug 'We create all non-existing coupons on stripe. This may take a while...'
|
||||
total = Coupon.all.count
|
||||
Coupon.all.each_with_index do |coupon, index|
|
||||
logger.debug "#{index} / #{total}"
|
||||
Stripe::Coupon.retrieve(c.code, api_key: Setting.get('stripe_secret_key'))
|
||||
rescue Stripe::InvalidRequestError
|
||||
Stripe::Service.create_coupon(c.id)
|
||||
end
|
||||
|
||||
w = StripeWorker.new
|
||||
[Machine, Training, Space, Plan].each do |klass|
|
||||
logger.debug "We create all non-existing #{klass} on stripe. This may take a while..."
|
||||
total = klass.all.count
|
||||
klass.all.each_with_index do |item, index|
|
||||
logger.debug "#{index} / #{total}"
|
||||
w.perform(:create_or_update_stp_product, klass.name, item.id)
|
||||
end
|
||||
end
|
||||
logger.debug 'Sync is done'
|
||||
return unless notify_user_id
|
||||
|
||||
logger.debug "Notify user #{notify_user_id}"
|
||||
user = User.find(notify_user_id)
|
||||
NotificationCenter.call type: :notify_admin_members_stripe_sync,
|
||||
NotificationCenter.call type: :notify_admin_objects_stripe_sync,
|
||||
receiver: user,
|
||||
attached_object: user
|
||||
end
|
@ -677,6 +677,7 @@ en:
|
||||
filter_schedules: "Filter schedules"
|
||||
no_payment_schedules: "No payment schedules to display"
|
||||
load_more: "Load more"
|
||||
card_updated_success: "The user's card was successfully updated"
|
||||
document_filters:
|
||||
reference: "Reference"
|
||||
customer: "Customer"
|
||||
|
@ -132,6 +132,7 @@ en:
|
||||
payment_schedules:
|
||||
no_payment_schedules: "No payment schedules to display"
|
||||
load_more: "Load more"
|
||||
card_updated_success: "Your card was successfully updated"
|
||||
#public profil of a member
|
||||
members_show:
|
||||
members_list: "Members list"
|
||||
|
@ -507,10 +507,17 @@ en:
|
||||
confirm_button: "Confirm"
|
||||
resolve_action: "Resolve the action"
|
||||
ok_button: "OK"
|
||||
validate_button: "Validate the new card"
|
||||
cancel_subscription: "Cancel the subscription"
|
||||
confirm_cancel_subscription: "You're about to cancel this payment schedule and the related subscription. Are you sure?"
|
||||
please_ask_reception: "For any questions, please contact the FabLab's reception."
|
||||
payment_modal:
|
||||
online_payment_disabled: "Online payment is not available. Please contact the FabLab's reception directly."
|
||||
unexpected_error: "An error occurred. Please report this issue to the Fab-Manager's team."
|
||||
update_card_modal:
|
||||
unexpected_error: "An error occurred. Please report this issue to the Fab-Manager's team."
|
||||
stripe_card_update_modal:
|
||||
update_card: "Update the card"
|
||||
validate_button: "Validate the new card"
|
||||
payzen_card_update_modal:
|
||||
update_card: "Update the card"
|
||||
validate_button: "Validate the new card"
|
||||
|
@ -354,8 +354,8 @@ en:
|
||||
your_role_is_ROLE: "Your role has been changed to %{ROLE}."
|
||||
notify_admins_role_update:
|
||||
user_NAME_changed_ROLE_html: "User <strong><em>%{NAME}</strong></em> is now %{ROLE}."
|
||||
notify_admin_members_stripe_sync:
|
||||
all_members_sync: "All members were successfully synchronized on Stripe."
|
||||
notify_admin_objects_stripe_sync:
|
||||
all_objects_sync: "All data were successfully synchronized on Stripe."
|
||||
notify_user_when_payment_schedule_ready:
|
||||
your_schedule_is_ready_html: "Your payment schedule #%{REFERENCE}, of %{AMOUNT}, is ready. <a href='api/payment_schedules/%{SCHEDULE_ID}/download' target='_blank'>Click here to download</a>."
|
||||
notify_admin_payment_schedule_failed:
|
||||
|
@ -285,10 +285,10 @@ en:
|
||||
subject: "Your role has changed"
|
||||
body:
|
||||
role_changed_html: "Your role at {GENDER, select, male{the} female{the} neutral{} other{the}} {NAME} has changed. You are now <strong>{ROLE}</strong>.<br/>With great power comes great responsibility, use your new privileges fairly and respectfully."
|
||||
notify_admin_members_stripe_sync:
|
||||
notify_admin_objects_stripe_sync:
|
||||
subject: "Stripe synchronization"
|
||||
body:
|
||||
members_sync: "All members were successfully synchronized on Stripe."
|
||||
objects_sync: "All members, coupons, machines, trainings, spaces and plans were successfully synchronized on Stripe."
|
||||
notify_member_payment_schedule_ready:
|
||||
subject: "Your payment schedule"
|
||||
body:
|
||||
|
@ -186,6 +186,7 @@ Rails.application.routes.draw do
|
||||
post 'payzen/confirm_payment_schedule' => 'payzen#confirm_payment_schedule'
|
||||
post 'payzen/check_hash' => 'payzen#check_hash'
|
||||
post 'payzen/create_token' => 'payzen#create_token'
|
||||
post 'payzen/update_token' => 'payzen#update_token'
|
||||
|
||||
# local payments handling
|
||||
post 'local_payment/confirm_payment' => 'local_payment#confirm_payment'
|
||||
|
@ -57,8 +57,12 @@ class PayZen::Helper
|
||||
|
||||
## Generate a hash map compatible with PayZen 'V4/Customer/ShoppingCart'
|
||||
def generate_shopping_cart(cart_items, customer, operator)
|
||||
cs = CartService.new(operator)
|
||||
cart = cs.from_hash(cart_items)
|
||||
cart = if cart_items.is_a? ShoppingCart
|
||||
cart_items
|
||||
else
|
||||
cs = CartService.new(operator)
|
||||
cs.from_hash(cart_items)
|
||||
end
|
||||
{
|
||||
cartItemInfo: cart.items.map do |item|
|
||||
{
|
||||
|
@ -26,4 +26,12 @@ class PayZen::Item < Payment::Item
|
||||
def payment_mean?
|
||||
klass == 'PayZen::Token'
|
||||
end
|
||||
|
||||
def subscription?
|
||||
klass == 'PayZen::Subscription'
|
||||
end
|
||||
|
||||
def order?
|
||||
klass == 'PayZen::Order'
|
||||
end
|
||||
end
|
||||
|
@ -49,39 +49,35 @@ class PayZen::Service < Payment::Service
|
||||
end
|
||||
|
||||
def process_payment_schedule_item(payment_schedule_item)
|
||||
pz_order = payment_schedule_item.payment_schedule.payment_gateway_objects.find { |pgo| pgo.gateway_object_type == 'PayZen::Order' }.gateway_object.retrieve
|
||||
pz_order = payment_schedule_item.payment_schedule.gateway_order.retrieve
|
||||
transaction = pz_order['answer']['transactions'].last
|
||||
transaction_date = DateTime.parse(transaction['creationDate']).to_date
|
||||
# check that the transaction matches the current PaymentScheduleItem
|
||||
if transaction['amount'] == payment_schedule_item.amount &&
|
||||
transaction_date >= payment_schedule_item.due_date.to_date &&
|
||||
transaction_date <= payment_schedule_item.due_date.to_date + 7.days
|
||||
if transaction['status'] == 'PAID'
|
||||
PaymentScheduleService.new.generate_invoice(payment_schedule_item,
|
||||
payment_method: 'card',
|
||||
payment_id: transaction['uuid'],
|
||||
payment_type: 'PayZen::Transaction')
|
||||
payment_schedule_item.update_attributes(state: 'paid', payment_method: 'card')
|
||||
pgo = PaymentGatewayObject.find_or_initialize_by(item: payment_schedule_item)
|
||||
pgo.gateway_object = PayZen::Item.new('PayZen::Transaction', transaction['uuid'])
|
||||
pgo.save!
|
||||
elsif transaction['status'] == 'RUNNING'
|
||||
if payment_schedule_item.state == 'new'
|
||||
# notify only for new deadlines, to prevent spamming
|
||||
NotificationCenter.call type: 'notify_admin_payment_schedule_failed',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: payment_schedule_item
|
||||
NotificationCenter.call type: 'notify_member_payment_schedule_failed',
|
||||
receiver: payment_schedule_item.payment_schedule.user,
|
||||
attached_object: payment_schedule_item
|
||||
end
|
||||
payment_schedule_item.update_attributes(state: transaction['detailedStatus'])
|
||||
pgo = PaymentGatewayObject.find_or_initialize_by(item: payment_schedule_item)
|
||||
pgo.gateway_object = PayZen::Item.new('PayZen::Transaction', transaction['uuid'])
|
||||
pgo.save!
|
||||
else
|
||||
payment_schedule_item.update_attributes(state: 'error')
|
||||
return unless transaction_matches?(transaction, payment_schedule_item)
|
||||
|
||||
if transaction['status'] == 'PAID'
|
||||
PaymentScheduleService.new.generate_invoice(payment_schedule_item,
|
||||
payment_method: 'card',
|
||||
payment_id: transaction['uuid'],
|
||||
payment_type: 'PayZen::Transaction')
|
||||
payment_schedule_item.update_attributes(state: 'paid', payment_method: 'card')
|
||||
pgo = PaymentGatewayObject.find_or_initialize_by(item: payment_schedule_item)
|
||||
pgo.gateway_object = PayZen::Item.new('PayZen::Transaction', transaction['uuid'])
|
||||
pgo.save!
|
||||
elsif transaction['status'] == 'RUNNING'
|
||||
if payment_schedule_item.state == 'new'
|
||||
# notify only for new deadlines, to prevent spamming
|
||||
NotificationCenter.call type: 'notify_admin_payment_schedule_failed',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: payment_schedule_item
|
||||
NotificationCenter.call type: 'notify_member_payment_schedule_failed',
|
||||
receiver: payment_schedule_item.payment_schedule.user,
|
||||
attached_object: payment_schedule_item
|
||||
end
|
||||
payment_schedule_item.update_attributes(state: transaction['detailedStatus'])
|
||||
pgo = PaymentGatewayObject.find_or_initialize_by(item: payment_schedule_item)
|
||||
pgo.gateway_object = PayZen::Item.new('PayZen::Transaction', transaction['uuid'])
|
||||
pgo.save!
|
||||
else
|
||||
payment_schedule_item.update_attributes(state: 'error')
|
||||
end
|
||||
end
|
||||
|
||||
@ -91,4 +87,13 @@ class PayZen::Service < Payment::Service
|
||||
count = payment_schedule.payment_schedule_items.count
|
||||
"RRULE:FREQ=MONTHLY;COUNT=#{count}"
|
||||
end
|
||||
|
||||
# check if the given transaction matches the given PaymentScheduleItem
|
||||
def transaction_matches?(transaction, payment_schedule_item)
|
||||
transaction_date = DateTime.parse(transaction['creationDate']).to_date
|
||||
|
||||
transaction['amount'] == payment_schedule_item.amount &&
|
||||
transaction_date >= payment_schedule_item.due_date.to_date &&
|
||||
transaction_date <= payment_schedule_item.due_date.to_date + 7.days
|
||||
end
|
||||
end
|
||||
|
@ -14,4 +14,15 @@ class PayZen::Token < PayZen::Client
|
||||
def get(payment_method_token)
|
||||
post('/Token/Get/', paymentMethodToken: payment_method_token)
|
||||
end
|
||||
|
||||
##
|
||||
# @see https://payzen.io/en-EN/rest/V4.0/api/playground/Token/Update/
|
||||
##
|
||||
def update(payment_method_token, customer, order_id: nil, currency: Setting.get('payzen_currency'))
|
||||
post('/Token/Update/',
|
||||
paymentMethodToken: payment_method_token,
|
||||
currency: currency,
|
||||
orderId: order_id,
|
||||
customer: customer)
|
||||
end
|
||||
end
|
||||
|
@ -21,5 +21,13 @@ class Payment::Item
|
||||
false
|
||||
end
|
||||
|
||||
def subscription?
|
||||
false
|
||||
end
|
||||
|
||||
def order?
|
||||
false
|
||||
end
|
||||
|
||||
def retrieve(_id = nil, *_args); end
|
||||
end
|
||||
|
@ -17,4 +17,8 @@ class Stripe::Item < Payment::Item
|
||||
def payment_mean?
|
||||
klass == 'Stripe::SetupIntent'
|
||||
end
|
||||
|
||||
def subscription?
|
||||
klass == 'Stripe::Subscription'
|
||||
end
|
||||
end
|
||||
|
@ -37,10 +37,10 @@ namespace :fablab do
|
||||
end
|
||||
end
|
||||
|
||||
desc 'sync users to the stripe database'
|
||||
task sync_members: :environment do
|
||||
puts 'We create all non-existing customers on stripe. This may take a while, please wait...'
|
||||
SyncMembersOnStripeWorker.new.perform
|
||||
desc 'sync all objects to the stripe API'
|
||||
task sync_objects: :environment do
|
||||
puts 'We create all non-existing objects on stripe. This may take a while, please wait...'
|
||||
SyncObjectsOnStripeWorker.new.perform
|
||||
puts 'Done'
|
||||
end
|
||||
desc 'sync coupons to the stripe database'
|
||||
@ -49,7 +49,7 @@ namespace :fablab do
|
||||
Coupon.all.each do |c|
|
||||
Stripe::Coupon.retrieve(c.code, api_key: Setting.get('stripe_secret_key'))
|
||||
rescue Stripe::InvalidRequestError
|
||||
StripeService.create_stripe_coupon(c.id)
|
||||
Stripe::Service.create_coupon(c.id)
|
||||
end
|
||||
puts 'Done'
|
||||
end
|
||||
|
@ -213,7 +213,7 @@ prepare_nginx()
|
||||
if [ "$confirm" != "n" ]; then
|
||||
echo "Adding a network configuration to the docker-compose.yml file..."
|
||||
yq -i eval '.networks.web.external = "true"' docker-compose.yml
|
||||
yq -i eval '.networks.db = null' docker-compose.yml
|
||||
yq -i eval '.networks.db = "" | .networks.db tag="!!null"' docker-compose.yml
|
||||
yq -i eval '.services.fabmanager.networks += ["web"]' docker-compose.yml
|
||||
yq -i eval '.services.fabmanager.networks += ["db"]' docker-compose.yml
|
||||
yq -i eval '.services.postgres.networks += ["db"]' docker-compose.yml
|
||||
|
Loading…
x
Reference in New Issue
Block a user