mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-19 13:54:25 +01:00
(feat) When a payment schedule is in error or canceled, ability to re-enable it with another payment method
This commit is contained in:
parent
d8f27f0b1a
commit
634da414f8
@ -6,7 +6,7 @@
|
||||
- Ability to select "bank transfer" as the payment mean for a payment schedule
|
||||
- When a payment schedule was canceled by the payment gateway, alert the users
|
||||
- When a payment schedule is in error, alert the users
|
||||
- Specilized VAT rate cannot be defined unless the VAT is enabled and saved
|
||||
- When a payment schedule is in error or canceled, ability to re-enable it with another payment method
|
||||
- Fix card image ratio
|
||||
- Update events heading style
|
||||
- Update some icons
|
||||
|
@ -3,7 +3,7 @@
|
||||
# API Controller for resources of PaymentSchedule
|
||||
class API::PaymentSchedulesController < API::ApiController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_payment_schedule, only: %i[download cancel]
|
||||
before_action :set_payment_schedule, only: %i[download cancel update]
|
||||
before_action :set_payment_schedule_item, only: %i[show_item cash_check confirm_transfer refresh_item pay_item]
|
||||
|
||||
# retrieve all payment schedules for the current user, paginated
|
||||
@ -85,6 +85,17 @@ class API::PaymentSchedulesController < API::ApiController
|
||||
render json: { canceled_at: canceled_at }, status: :ok
|
||||
end
|
||||
|
||||
## Only the update of the payment method is allowed
|
||||
def update
|
||||
authorize PaymentSchedule
|
||||
|
||||
if PaymentScheduleService.new.update_payment_mean(@payment_schedule, update_params)
|
||||
render :show, status: :ok, location: @payment_schedule
|
||||
else
|
||||
render json: @payment_schedule.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_payment_schedule
|
||||
@ -94,4 +105,8 @@ class API::PaymentSchedulesController < API::ApiController
|
||||
def set_payment_schedule_item
|
||||
@payment_schedule_item = PaymentScheduleItem.find(params[:id])
|
||||
end
|
||||
|
||||
def update_params
|
||||
params.require(:payment_schedule).permit(:payment_method)
|
||||
end
|
||||
end
|
||||
|
@ -47,4 +47,9 @@ export default class PaymentScheduleAPI {
|
||||
const res: AxiosResponse = await apiClient.put(`/api/payment_schedules/${paymentScheduleId}/cancel`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async update (paymentSchedule: PaymentSchedule): Promise<PaymentSchedule> {
|
||||
const res:AxiosResponse<PaymentSchedule> = await apiClient.patch(`/api/payment_schedules/${paymentSchedule.id}`, paymentSchedule);
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
@ -13,11 +13,13 @@ import { FabModal } from '../base/fab-modal';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { StripeConfirmModal } from '../payment/stripe/stripe-confirm-modal';
|
||||
import { UpdateCardModal } from '../payment/update-card-modal';
|
||||
import { UpdatePaymentMeanModal } from './update-payment-mean-modal';
|
||||
|
||||
// we want to display some buttons only once. This is the types of buttons it applies to.
|
||||
export enum TypeOnce {
|
||||
CardUpdate = 'card-update',
|
||||
SubscriptionCancel = 'subscription-cancel',
|
||||
UpdatePaymentMean = 'update-payment-mean'
|
||||
}
|
||||
|
||||
interface PaymentScheduleItemActionsProps {
|
||||
@ -47,6 +49,8 @@ export const PaymentScheduleItemActions: React.FC<PaymentScheduleItemActionsProp
|
||||
const [showResolveAction, setShowResolveAction] = useState<boolean>(false);
|
||||
// is open, the modal dialog to update the card details
|
||||
const [showUpdateCard, setShowUpdateCard] = useState<boolean>(false);
|
||||
// is open, the modal dialog to update the payment mean
|
||||
const [showUpdatePaymentMean, setShowUpdatePaymentMean] = useState<boolean>(false);
|
||||
// the user cannot confirm the action modal (3D secure), unless he has resolved the pending action
|
||||
const [isConfirmActionDisabled, setConfirmActionDisabled] = useState<boolean>(true);
|
||||
|
||||
@ -130,6 +134,23 @@ export const PaymentScheduleItemActions: React.FC<PaymentScheduleItemActionsProp
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a button to update the default payment mean for the current payment schedule
|
||||
*/
|
||||
const updatePaymentMeanButton = (): ReactElement => {
|
||||
const displayOnceStatus = displayOnceMap.get(TypeOnce.UpdatePaymentMean).get(paymentSchedule.id);
|
||||
if (isPrivileged() && (!displayOnceStatus || displayOnceStatus === paymentScheduleItem.id)) {
|
||||
displayOnceMap.get(TypeOnce.UpdatePaymentMean).set(paymentSchedule.id, paymentScheduleItem.id);
|
||||
return (
|
||||
<FabButton key={`update-payment-mean-${paymentScheduleItem.id}`}
|
||||
onClick={toggleUpdatePaymentMeanModal}
|
||||
icon={<i className="fas fa-money-bill-alt" />}>
|
||||
{t('app.shared.payment_schedule_item_actions.update_payment_mean')}
|
||||
</FabButton>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a button to update the credit card associated with the payment schedule
|
||||
*/
|
||||
@ -166,14 +187,18 @@ export const PaymentScheduleItemActions: React.FC<PaymentScheduleItemActionsProp
|
||||
/**
|
||||
* Return the actions button(s) for current paymentScheduleItem with state Error or GatewayCanceled
|
||||
*/
|
||||
const errorActions = (): ReactElement => {
|
||||
const errorActions = (): ReactElement[] => {
|
||||
// if the payment schedule is canceled/in error, the schedule is over, and we can't update the card
|
||||
displayOnceMap.get(TypeOnce.CardUpdate).set(paymentSchedule.id, paymentScheduleItem.id);
|
||||
|
||||
const buttons = [];
|
||||
if (isPrivileged()) {
|
||||
return cancelSubscriptionButton();
|
||||
buttons.push(cancelSubscriptionButton());
|
||||
buttons.push(updatePaymentMeanButton());
|
||||
} else {
|
||||
return <span>{t('app.shared.payment_schedule_item_actions.please_ask_reception')}</span>;
|
||||
buttons.push(<span>{t('app.shared.payment_schedule_item_actions.please_ask_reception')}</span>);
|
||||
}
|
||||
return buttons;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -232,6 +257,13 @@ export const PaymentScheduleItemActions: React.FC<PaymentScheduleItemActionsProp
|
||||
setShowUpdateCard(!showUpdateCard);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog to update the payment mean
|
||||
*/
|
||||
const toggleUpdatePaymentMeanModal = (): void => {
|
||||
setShowUpdatePaymentMean(!showUpdatePaymentMean);
|
||||
};
|
||||
|
||||
/**
|
||||
* After the user has confirmed that he wants to cash the check, update the API, refresh the list and close the modal.
|
||||
*/
|
||||
@ -245,10 +277,10 @@ export const PaymentScheduleItemActions: React.FC<PaymentScheduleItemActionsProp
|
||||
};
|
||||
|
||||
/**
|
||||
* After the user has confirmed that he validates the tranfer, update the API, refresh the list and close the modal.
|
||||
* After the user has confirmed that he validates the transfer, update the API, refresh the list and close the modal.
|
||||
*/
|
||||
const onTransferConfirmed = (): void => {
|
||||
PaymentScheduleAPI.confirmTransfer(paymentSchedule.id).then((res) => {
|
||||
PaymentScheduleAPI.confirmTransfer(paymentScheduleItem.id).then((res) => {
|
||||
if (res.state === PaymentScheduleItemState.Paid) {
|
||||
onSuccess();
|
||||
toggleConfirmTransferModal();
|
||||
@ -297,6 +329,14 @@ export const PaymentScheduleItemActions: React.FC<PaymentScheduleItemActionsProp
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* When the update of the payment mean was successful, refresh the list and close the modal
|
||||
*/
|
||||
const onPaymentMeanUpdateSuccess = (): void => {
|
||||
onSuccess();
|
||||
toggleUpdatePaymentMeanModal();
|
||||
};
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
@ -359,6 +399,12 @@ export const PaymentScheduleItemActions: React.FC<PaymentScheduleItemActionsProp
|
||||
onError={onError}
|
||||
schedule={paymentSchedule}>
|
||||
</UpdateCardModal>
|
||||
{/* Update the payment mean */}
|
||||
<UpdatePaymentMeanModal isOpen={showUpdatePaymentMean}
|
||||
toggleModal={toggleUpdatePaymentMeanModal}
|
||||
onError={onError}
|
||||
afterSuccess={onPaymentMeanUpdateSuccess}
|
||||
paymentSchedule={paymentSchedule} />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
@ -32,7 +32,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
// we want to display some buttons only once. This map keep track of the buttons that have been displayed.
|
||||
const [displayOnceMap] = useState<Map<TypeOnce, Map<number, number>>>(new Map([
|
||||
[TypeOnce.SubscriptionCancel, new Map()],
|
||||
[TypeOnce.CardUpdate, new Map()]
|
||||
[TypeOnce.CardUpdate, new Map()],
|
||||
[TypeOnce.UpdatePaymentMean, new Map()]
|
||||
]));
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { PaymentMethod, PaymentSchedule } from '../../models/payment-schedule';
|
||||
import PaymentScheduleAPI from '../../api/payment-schedule';
|
||||
|
||||
interface UpdatePaymentMeanModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
onError: (message: string) => void,
|
||||
afterSuccess: () => void,
|
||||
paymentSchedule: PaymentSchedule
|
||||
}
|
||||
|
||||
/**
|
||||
* Option format, expected by react-select
|
||||
* @see https://github.com/JedWatson/react-select
|
||||
*/
|
||||
type selectOption = { value: PaymentMethod, label: string };
|
||||
|
||||
export const UpdatePaymentMeanModal: React.FC<UpdatePaymentMeanModalProps> = ({ isOpen, toggleModal, onError, afterSuccess, paymentSchedule }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [paymentMean, setPaymentMean] = React.useState<PaymentMethod>();
|
||||
|
||||
/**
|
||||
* Convert all payment means to the react-select format
|
||||
*/
|
||||
const buildOptions = (): Array<selectOption> => {
|
||||
return Object.keys(PaymentMethod).filter(pm => PaymentMethod[pm] !== PaymentMethod.Card).map(pm => {
|
||||
return { value: PaymentMethod[pm], label: t(`app.admin.update_payment_mean_modal.method_${pm}`) };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* When the payment mean is changed in the select, update the state
|
||||
*/
|
||||
const handleMeanSelected = (option: selectOption): void => {
|
||||
setPaymentMean(option.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* When the user clicks on the update button, update the default payment mean for the given payment schedule
|
||||
*/
|
||||
const handlePaymentMeanUpdate = (): void => {
|
||||
PaymentScheduleAPI.update({
|
||||
id: paymentSchedule.id,
|
||||
payment_method: paymentMean
|
||||
}).then(() => {
|
||||
afterSuccess();
|
||||
}).catch(error => {
|
||||
onError(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal isOpen={isOpen}
|
||||
className="update-payment-mean-modal"
|
||||
title={t('app.admin.update_payment_mean_modal.title')}
|
||||
confirmButton={t('app.admin.update_payment_mean_modal.confirm_button')}
|
||||
onConfirm={handlePaymentMeanUpdate}
|
||||
toggleModal={toggleModal}
|
||||
closeButton={true}>
|
||||
<span>{t('app.admin.update_payment_mean_modal.update_info')}</span>
|
||||
<Select className="payment-mean-select"
|
||||
placeholder={t('app.admin.update_payment_mean_modal.select_payment_mean')}
|
||||
id="payment-mean"
|
||||
onChange={handleMeanSelected}
|
||||
options={buildOptions()}></Select>
|
||||
</FabModal>
|
||||
);
|
||||
};
|
@ -476,8 +476,6 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
controller: ['$scope', '$uibModalInstance', 'rate', 'active', 'rateHistory', 'activeHistory', 'multiVAT', function ($scope, $uibModalInstance, rate, active, rateHistory, activeHistory, multiVAT) {
|
||||
$scope.rate = rate;
|
||||
$scope.isSelected = active;
|
||||
// this one is read only
|
||||
$scope.isActive = active;
|
||||
$scope.history = [];
|
||||
|
||||
// callback on "enable VAT" switch toggle
|
||||
|
@ -25,28 +25,28 @@ export interface PaymentScheduleItem {
|
||||
}
|
||||
|
||||
export interface PaymentSchedule {
|
||||
max_length: number;
|
||||
max_length?: number;
|
||||
id: number,
|
||||
total: number,
|
||||
reference: string,
|
||||
total?: number,
|
||||
reference?: string,
|
||||
payment_method: PaymentMethod,
|
||||
items: Array<PaymentScheduleItem>,
|
||||
created_at: Date,
|
||||
chained_footprint: boolean,
|
||||
main_object: {
|
||||
items?: Array<PaymentScheduleItem>,
|
||||
created_at?: Date,
|
||||
chained_footprint?: boolean,
|
||||
main_object?: {
|
||||
type: string,
|
||||
id: number
|
||||
},
|
||||
user: {
|
||||
user?: {
|
||||
id: number,
|
||||
name: string
|
||||
},
|
||||
operator: {
|
||||
operator?: {
|
||||
id: number,
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
},
|
||||
gateway: 'PayZen' | 'Stripe',
|
||||
gateway?: 'PayZen' | 'Stripe',
|
||||
}
|
||||
|
||||
export interface PaymentScheduleIndexRequest {
|
||||
|
@ -51,7 +51,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-warning pull-left" ng-click="editMultiVAT()" ng-show="isActive" translate>{{ 'app.admin.invoices.edit_multi_VAT_button' }}</button>
|
||||
<button class="btn btn-warning pull-left" ng-click="editMultiVAT()" ng-show="isSelected" translate>{{ 'app.admin.invoices.edit_multi_VAT_button' }}</button>
|
||||
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
|
||||
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
# Check the access policies for API::PaymentSchedulesController
|
||||
class PaymentSchedulePolicy < ApplicationPolicy
|
||||
%w[list? cash_check? confirm_transfer? cancel?].each do |action|
|
||||
%w[list? cash_check? confirm_transfer? cancel? update?].each do |action|
|
||||
define_method action do
|
||||
user.admin? || user.manager?
|
||||
end
|
||||
|
@ -157,6 +157,10 @@ class PaymentScheduleService
|
||||
ps
|
||||
end
|
||||
|
||||
##
|
||||
# Cancel the given PaymentSchedule: cancel the remote subscription on the payment gateway, mark the PaymentSchedule as cancelled,
|
||||
# the remaining PaymentScheduleItems as canceled too, and cancel the associated Subscription.
|
||||
##
|
||||
def self.cancel(payment_schedule)
|
||||
PaymentGatewayService.new.cancel_subscription(payment_schedule)
|
||||
|
||||
@ -173,8 +177,26 @@ class PaymentScheduleService
|
||||
subscription.canceled_at
|
||||
end
|
||||
|
||||
##
|
||||
# Update the payment mean associated with the given PaymentSchedule and reset the erroneous items
|
||||
##
|
||||
def update_payment_mean(payment_schedule, payment_mean)
|
||||
payment_schedule.update(payment_mean) && reset_erroneous_payment_schedule_items(payment_schedule)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
##
|
||||
# After the payment method has been updated, we need to reset the erroneous payment schedule items
|
||||
# so the admin can confirm them to generate the invoice
|
||||
##
|
||||
def reset_erroneous_payment_schedule_items(payment_schedule)
|
||||
results = payment_schedule.payment_schedule_items.where(state: %w[error gateway_canceled]).map do |item|
|
||||
item.update_attributes(state: item.due_date < DateTime.current ? 'pending' : 'new')
|
||||
end
|
||||
results.reduce(true) { |acc, item| acc && item }
|
||||
end
|
||||
|
||||
##
|
||||
# The first PaymentScheduleItem contains references to the reservation price (if any) and to the adjustment price
|
||||
# for the subscription (if any) and the wallet transaction (if any)
|
||||
|
@ -19,7 +19,7 @@ json.main_object do
|
||||
end
|
||||
if payment_schedule.gateway_subscription
|
||||
# this attribute is used to known which gateway should we interact with, in the front-end
|
||||
json.gateway json.classname payment_schedule.gateway_subscription.gateway
|
||||
json.gateway payment_schedule.gateway_subscription.gateway
|
||||
end
|
||||
json.items payment_schedule.payment_schedule_items do |item|
|
||||
json.partial! 'api/payment_schedules/payment_schedule_item', item: item
|
||||
|
@ -1,3 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'api/payment_schedules/payment_schedule', payment_schedule: @payment_schedule_item.payment_schedule
|
||||
json.partial! 'api/payment_schedules/payment_schedule', payment_schedule: @payment_schedule
|
||||
|
@ -1,7 +1,7 @@
|
||||
json.setting do
|
||||
json.partial! 'api/settings/setting', setting: @setting
|
||||
if @show_history
|
||||
json.history @setting.history_values do |value|
|
||||
json.history @setting.history_values.includes(:invoicing_profile) do |value|
|
||||
json.extract! value, :id, :value, :created_at
|
||||
unless value.invoicing_profile.nil?
|
||||
json.user do
|
||||
|
@ -762,6 +762,13 @@ en:
|
||||
reference: "Reference"
|
||||
customer: "Customer"
|
||||
date: "Date"
|
||||
update_payment_mean_modal:
|
||||
title: "Update the payment mean"
|
||||
update_info: "Please specify below the new payment mean for this payment schedule to continue."
|
||||
select_payment_mean: "Select a new payment mean"
|
||||
method_Transfer: "By bank transfer"
|
||||
method_Check: "By check"
|
||||
confirm_button: "Update"
|
||||
#management of users, labels, groups, and so on
|
||||
members:
|
||||
users_management: "Users management"
|
||||
|
@ -505,6 +505,7 @@ en:
|
||||
confirm_check: "Confirm cashing"
|
||||
resolve_action: "Resolve the action"
|
||||
update_card: "Update the card"
|
||||
update_payment_mean: "Update the payment mean"
|
||||
please_ask_reception: "For any questions, please contact the FabLab's reception."
|
||||
confirm_button: "Confirm"
|
||||
confirm_check_cashing: "Confirm the cashing of the check"
|
||||
|
@ -376,9 +376,9 @@ en:
|
||||
notify_member_payment_schedule_failed:
|
||||
schedule_failed: "Failed card debit for the %{DATE} deadline, for your schedule %{REFERENCE}"
|
||||
notify_admin_payment_schedule_gateway_canceled:
|
||||
schedule_error: "The payment schedule %{REFERENCE} was canceled by the gateway. An action is required."
|
||||
schedule_canceled: "The payment schedule %{REFERENCE} was canceled by the gateway. An action is required."
|
||||
notify_member_payment_schedule_gateway_canceled:
|
||||
schedule_error: "Your payment schedule %{REFERENCE} was canceled by the gateway."
|
||||
schedule_canceled: "Your payment schedule %{REFERENCE} was canceled by the gateway."
|
||||
notify_admin_payment_schedule_check_deadline:
|
||||
schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
notify_admin_payment_schedule_transfer_deadline:
|
||||
|
@ -119,7 +119,7 @@ Rails.application.routes.draw do
|
||||
get 'first', action: 'first', on: :collection
|
||||
end
|
||||
|
||||
resources :payment_schedules, only: %i[index show] do
|
||||
resources :payment_schedules, only: %i[index show update] do
|
||||
post 'list', action: 'list', on: :collection
|
||||
put 'cancel', on: :member
|
||||
get 'download', on: :member
|
||||
|
Loading…
x
Reference in New Issue
Block a user