mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-17 06:52:27 +01:00
Ability to select "bank transfer" as the payment mean for a payment schedule
This commit is contained in:
parent
d7ccbdbb52
commit
9922812111
@ -3,6 +3,7 @@
|
||||
- Ability to cancel a payement schedule from the interface
|
||||
- Ability to create slots in the past
|
||||
- Ability to disable public account creation
|
||||
- Ability to select "bank transfer" as the payment mean for a payment schedule
|
||||
- Updated caniuse db
|
||||
- Optimized the load time of the payment schedules list
|
||||
- Fix a bug: do not load Stripe if no keys were defined
|
||||
|
@ -4,7 +4,7 @@
|
||||
class API::PaymentSchedulesController < API::ApiController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_payment_schedule, only: %i[download cancel]
|
||||
before_action :set_payment_schedule_item, only: %i[show_item cash_check refresh_item pay_item]
|
||||
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
|
||||
def index
|
||||
@ -46,6 +46,15 @@ class API::PaymentSchedulesController < API::ApiController
|
||||
render json: attrs, status: :ok
|
||||
end
|
||||
|
||||
def confirm_transfer
|
||||
authorize @payment_schedule_item.payment_schedule
|
||||
PaymentScheduleService.new.generate_invoice(@payment_schedule_item, payment_method: 'transfer')
|
||||
attrs = { state: 'paid', payment_method: 'transfer' }
|
||||
@payment_schedule_item.update_attributes(attrs)
|
||||
|
||||
render json: attrs, status: :ok
|
||||
end
|
||||
|
||||
def refresh_item
|
||||
authorize @payment_schedule_item.payment_schedule
|
||||
PaymentScheduleItemWorker.new.perform(@payment_schedule_item.id)
|
||||
|
@ -23,6 +23,11 @@ export default class PaymentScheduleAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async confirmTransfer (paymentScheduleItemId: number): Promise<CashCheckResponse> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/confirm_transfer`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async getItem (paymentScheduleItemId: number): Promise<PaymentScheduleItem> {
|
||||
const res: AxiosResponse = await apiClient.get(`/api/payment_schedules/items/${paymentScheduleItemId}`);
|
||||
return res?.data;
|
||||
|
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface HtmlTranslateProps {
|
||||
trKey: string,
|
||||
options?: Record<string, string>
|
||||
options?: Record<string, string|number>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,6 +31,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
const [showExpanded, setShowExpanded] = useState<Map<number, boolean>>(new Map());
|
||||
// is open, the modal dialog to confirm the cashing of a check?
|
||||
const [showConfirmCashing, setShowConfirmCashing] = useState<boolean>(false);
|
||||
// is open, the modal dialog to confirm a back transfer?
|
||||
const [showConfirmTransfer, setShowConfirmTransfer] = useState<boolean>(false);
|
||||
// is open, the modal dialog the resolve a pending card payment?
|
||||
const [showResolveAction, setShowResolveAction] = useState<boolean>(false);
|
||||
// the user cannot confirm the action modal (3D secure), unless he has resolved the pending action
|
||||
@ -130,8 +132,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
/**
|
||||
* Return the human-readable string for the status of the provided deadline.
|
||||
*/
|
||||
const formatState = (item: PaymentScheduleItem): JSX.Element => {
|
||||
let res = t(`app.shared.schedules_table.state_${item.state}`);
|
||||
const formatState = (item: PaymentScheduleItem, schedule: PaymentSchedule): JSX.Element => {
|
||||
let res = t(`app.shared.schedules_table.state_${item.state}${item.state === 'pending' ? '_' + schedule.payment_method : ''}`);
|
||||
if (item.state === PaymentScheduleItemState.Paid) {
|
||||
const key = `app.shared.schedules_table.method_${item.payment_method}`;
|
||||
res += ` (${t(key)})`;
|
||||
@ -155,12 +157,21 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
return downloadButton(TargetType.Invoice, item.invoice_id);
|
||||
case PaymentScheduleItemState.Pending:
|
||||
if (isPrivileged()) {
|
||||
return (
|
||||
<FabButton onClick={handleConfirmCheckPayment(item)}
|
||||
icon={<i className="fas fa-money-check" />}>
|
||||
{t('app.shared.schedules_table.confirm_payment')}
|
||||
</FabButton>
|
||||
);
|
||||
if (schedule.payment_method === 'transfer') {
|
||||
return (
|
||||
<FabButton onClick={handleConfirmTransferPayment(item)}
|
||||
icon={<i className="fas fa-university"/>}>
|
||||
{t('app.shared.schedules_table.confirm_payment')}
|
||||
</FabButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FabButton onClick={handleConfirmCheckPayment(item)}
|
||||
icon={<i className="fas fa-money-check"/>}>
|
||||
{t('app.shared.schedules_table.confirm_payment')}
|
||||
</FabButton>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>;
|
||||
}
|
||||
@ -216,6 +227,15 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user's clicks on the "confirm transfer" button: show a confirmation modal
|
||||
*/
|
||||
const handleConfirmTransferPayment = (item: PaymentScheduleItem): ReactEventHandler => {
|
||||
return (): void => {
|
||||
setTempDeadline(item);
|
||||
toggleConfirmTransferModal();
|
||||
};
|
||||
};
|
||||
/**
|
||||
* After the user has confirmed that he wants to cash the check, update the API, refresh the list and close the modal.
|
||||
*/
|
||||
@ -228,6 +248,18 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* After the user has confirmed that he validates the tranfer, update the API, refresh the list and close the modal.
|
||||
*/
|
||||
const onTransferConfirmed = (): void => {
|
||||
PaymentScheduleAPI.confirmTransfer(tempDeadline.id).then((res) => {
|
||||
if (res.state === PaymentScheduleItemState.Paid) {
|
||||
refreshSchedulesTable();
|
||||
toggleConfirmTransferModal();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh all payment schedules in the table
|
||||
*/
|
||||
@ -242,6 +274,13 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
setShowConfirmCashing(!showConfirmCashing);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog that enable to confirm the bank transfer for a given deadline.
|
||||
*/
|
||||
const toggleConfirmTransferModal = (): void => {
|
||||
setShowConfirmTransfer(!showConfirmTransfer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog that trigger the card "action".
|
||||
*/
|
||||
@ -392,7 +431,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
{_.orderBy(p.items, 'due_date').map(item => <tr key={item.id}>
|
||||
<td>{FormatLib.date(item.due_date)}</td>
|
||||
<td>{FormatLib.price(item.amount)}</td>
|
||||
<td>{formatState(item)}</td>
|
||||
<td>{formatState(item, p)}</td>
|
||||
<td>{itemButtons(item, p)}</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
@ -421,6 +460,20 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
})}
|
||||
</span>}
|
||||
</FabModal>
|
||||
{/* Confirm the bank transfer for the current deadline */}
|
||||
<FabModal title={t('app.shared.schedules_table.confirm_bank_transfer')}
|
||||
isOpen={showConfirmTransfer}
|
||||
toggleModal={toggleConfirmTransferModal}
|
||||
onConfirm={onTransferConfirmed}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.shared.schedules_table.confirm_button')}>
|
||||
{tempDeadline && <span>
|
||||
{t('app.shared.schedules_table.confirm_bank_transfer_body', {
|
||||
AMOUNT: FormatLib.price(tempDeadline.amount),
|
||||
DATE: FormatLib.date(tempDeadline.due_date)
|
||||
})}
|
||||
</span>}
|
||||
</FabModal>
|
||||
{/* Cancel the subscription */}
|
||||
<FabModal title={t('app.shared.schedules_table.cancel_subscription')}
|
||||
isOpen={showCancelSubscription}
|
||||
|
@ -8,9 +8,9 @@ import SettingAPI from '../../../api/setting';
|
||||
import { SettingName } from '../../../models/setting';
|
||||
import { PaymentModal } from '../payment-modal';
|
||||
import { PaymentSchedule } from '../../../models/payment-schedule';
|
||||
import { PaymentMethod } from '../../../models/payment';
|
||||
import { HtmlTranslate } from '../../base/html-translate';
|
||||
|
||||
const ALL_SCHEDULE_METHODS = ['card', 'check'] as const;
|
||||
const ALL_SCHEDULE_METHODS = ['card', 'check', 'transfer'] as const;
|
||||
type scheduleMethod = typeof ALL_SCHEDULE_METHODS[number];
|
||||
|
||||
/**
|
||||
@ -31,11 +31,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
const [onlinePaymentModal, setOnlinePaymentModal] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (cart.payment_method === PaymentMethod.Card) {
|
||||
setMethod('card');
|
||||
} else {
|
||||
setMethod('check');
|
||||
}
|
||||
setMethod(cart.payment_method || 'check');
|
||||
}, [cart]);
|
||||
|
||||
/**
|
||||
@ -65,11 +61,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
* Callback triggered when the user selects a payment method for the current payment schedule.
|
||||
*/
|
||||
const handleUpdateMethod = (option: selectOption) => {
|
||||
if (option.value === 'card') {
|
||||
updateCart(Object.assign({}, cart, { payment_method: PaymentMethod.Card }));
|
||||
} else {
|
||||
updateCart(Object.assign({}, cart, { payment_method: PaymentMethod.Other }));
|
||||
}
|
||||
updateCart(Object.assign({}, cart, { payment_method: option.value }));
|
||||
setMethod(option.value);
|
||||
};
|
||||
|
||||
@ -140,6 +132,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
value={methodToOption(method)} />
|
||||
{method === 'card' && <p>{t('app.admin.local_payment.card_collection_info')}</p>}
|
||||
{method === 'check' && <p>{t('app.admin.local_payment.check_collection_info', { DEADLINES: paymentSchedule.items.length })}</p>}
|
||||
{method === 'transfer' && <HtmlTranslate trKey="app.admin.local_payment.transfer_collection_info" options={{ DEADLINES: paymentSchedule.items.length }} />}
|
||||
</div>
|
||||
<div className="full-schedule">
|
||||
<ul>
|
||||
|
@ -59,6 +59,7 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
|
||||
case 'PayZen':
|
||||
return renderPayZenModal();
|
||||
case '':
|
||||
case undefined:
|
||||
return <div/>;
|
||||
default:
|
||||
onError(t('app.shared.update_card_modal.unexpected_error'));
|
||||
|
@ -26,7 +26,7 @@ export interface PaymentSchedule {
|
||||
id: number,
|
||||
total: number,
|
||||
reference: string,
|
||||
payment_method: 'card' | '',
|
||||
payment_method: 'card' | 'transfer' | '',
|
||||
items: Array<PaymentScheduleItem>,
|
||||
created_at: Date,
|
||||
chained_footprint: boolean,
|
||||
|
@ -18,7 +18,8 @@ export interface IntentConfirmation {
|
||||
|
||||
export enum PaymentMethod {
|
||||
Card = 'card',
|
||||
Other = ''
|
||||
Check = 'check',
|
||||
Transfer = 'transfer'
|
||||
}
|
||||
|
||||
export type CartItem = { reservation: Reservation }|
|
||||
|
@ -58,6 +58,7 @@ class NotificationType
|
||||
notify_admin_payment_schedule_failed
|
||||
notify_member_payment_schedule_failed
|
||||
notify_admin_payment_schedule_check_deadline
|
||||
notify_admin_payment_schedule_transfer_deadline
|
||||
]
|
||||
# deprecated:
|
||||
# - notify_member_subscribed_plan_is_changed
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
# Check the access policies for API::PaymentSchedulesController
|
||||
class PaymentSchedulePolicy < ApplicationPolicy
|
||||
%w[list? cash_check? cancel?].each do |action|
|
||||
%w[list? cash_check? confirm_transfer? cancel?].each do |action|
|
||||
define_method action do
|
||||
user.admin? || user.manager?
|
||||
end
|
||||
|
@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.title notification.notification_type
|
||||
json.description t('.schedule_deadline', DATE: I18n.l(notification.attached_object.due_date.to_date),
|
||||
REFERENCE: notification.attached_object.payment_schedule.reference)
|
@ -0,0 +1,10 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p>
|
||||
<%= t('.body.remember',
|
||||
REFERENCE: @attached_object.payment_schedule.reference,
|
||||
AMOUNT: number_to_currency(@attached_object.amount / 100.00),
|
||||
DATE: I18n.l(@attached_object.due_date, format: :long)) %>
|
||||
<%= t('.body.date') %>
|
||||
</p>
|
||||
<p><%= t('.body.confirm') %></p>
|
@ -22,8 +22,8 @@ class PaymentScheduleItemWorker
|
||||
### Cards
|
||||
PaymentGatewayService.new.process_payment_schedule_item(psi)
|
||||
elsif psi.state == 'new'
|
||||
### Check (only new deadlines, to prevent spamming)
|
||||
NotificationCenter.call type: 'notify_admin_payment_schedule_check_deadline',
|
||||
### Check/Bank transfer (only new deadlines, to prevent spamming)
|
||||
NotificationCenter.call type: "notify_admin_payment_schedule_#{psi.payment_schedule.payment_method}_deadline",
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: psi
|
||||
psi.update_attributes(state: 'pending')
|
||||
|
@ -1451,8 +1451,10 @@ en:
|
||||
payment_method: "Payment method"
|
||||
method_card: "Online by card"
|
||||
method_check: "By check"
|
||||
method_transfer: "By bank transfer"
|
||||
card_collection_info: "By validating, you'll be prompted for the member's card number. This card will be automatically charged at the deadlines."
|
||||
check_collection_info: "By validating, you confirm that you have {DEADLINES} checks, allowing you to collect all the monthly payments."
|
||||
transfer_collection_info: "<p>By validating, you confirm that you set up {DEADLINES} bank direct debits, allowing you to collect all the monthly payments.</p><p><strong>Please note:</strong> the bank transfers are not automatically handled by Fab-manager.</p>"
|
||||
online_payment_disabled: "Online payment is not available. You cannot collect this payment schedule by online card."
|
||||
check_list_setting:
|
||||
save: 'Save'
|
||||
|
@ -487,7 +487,8 @@ en:
|
||||
state: "State"
|
||||
download: "Download"
|
||||
state_new: "Not yet due"
|
||||
state_pending: "Waiting for the cashing of the check"
|
||||
state_pending_check: "Waiting for the cashing of the check"
|
||||
state_pending_transfer: "Waiting for the tranfer confirmation"
|
||||
state_requires_payment_method: "The credit card must be updated"
|
||||
state_requires_action: "Action required"
|
||||
state_paid: "Paid"
|
||||
@ -495,11 +496,14 @@ en:
|
||||
state_canceled: "Canceled"
|
||||
method_card: "by card"
|
||||
method_check: "by check"
|
||||
method_transfer: "by transfer"
|
||||
confirm_payment: "Confirm payment"
|
||||
solve: "Solve"
|
||||
update_card: "Update the card"
|
||||
confirm_check_cashing: "Confirm the cashing of the check"
|
||||
confirm_check_cashing_body: "You must cash a check of {AMOUNT} for the deadline of {DATE}. By confirming the cashing of the check, an invoice will be generated for this due date."
|
||||
confirm_bank_transfer: "Confirm the bank transfer"
|
||||
confirm_bank_transfer_body: "You must confirm the receipt of {AMOUNT} for the deadline of {DATE}. By confirming the bank transfer, an invoice will be generated for this due date."
|
||||
confirm_button: "Confirm"
|
||||
resolve_action: "Resolve the action"
|
||||
ok_button: "OK"
|
||||
|
@ -126,7 +126,7 @@ en:
|
||||
deadline_date: "Payment date"
|
||||
deadline_amount: "Amount including tax"
|
||||
total_amount: "Total amount"
|
||||
settlement_by_METHOD: "Debits will be made by {METHOD, select, card{card} other{check}} for each deadlines."
|
||||
settlement_by_METHOD: "Debits will be made by {METHOD, select, card{card} transfer{bank transfer} other{check}} for each deadlines."
|
||||
settlement_by_wallet: "%{AMOUNT} will be debited from your wallet to settle the first deadline."
|
||||
# CVS accounting export (columns headers)
|
||||
accounting_export:
|
||||
@ -373,6 +373,8 @@ en:
|
||||
schedule_failed: "Failed card debit for the %{DATE} deadline, for your schedule %{REFERENCE}"
|
||||
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:
|
||||
schedule_deadline: "You must confirm the bank direct debit for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
#statistics tools for admins
|
||||
statistics:
|
||||
subscriptions: "Subscriptions"
|
||||
|
@ -315,5 +315,11 @@ en:
|
||||
remember: "In accordance with the %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}."
|
||||
date: "This is a reminder to cash the scheduled check as soon as possible."
|
||||
confirm: "Do not forget to confirm the receipt in your payment schedule management interface, so that the corresponding invoice will be generated."
|
||||
notify_member_payment_schedule_transfer_deadline:
|
||||
subject: "Payment deadline"
|
||||
body:
|
||||
remember: "In accordance with your %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}."
|
||||
date: "This is a reminder to verify that the direct bank debit was successfull."
|
||||
confirm: "Please confirm the receipt of funds in your payment schedule management interface, so that the corresponding invoice will be generated."
|
||||
shared:
|
||||
hello: "Hello %{user_name}"
|
||||
|
@ -125,6 +125,7 @@ Rails.application.routes.draw do
|
||||
get 'download', on: :member
|
||||
get 'items/:id', action: 'show_item', on: :collection
|
||||
post 'items/:id/cash_check', action: 'cash_check', on: :collection
|
||||
post 'items/:id/confirm_transfer', action: 'confirm_transfer', on: :collection
|
||||
post 'items/:id/refresh_item', action: 'refresh_item', on: :collection
|
||||
post 'items/:id/pay_item', action: 'pay_item', on: :collection
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user