1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-30 19:52:20 +01:00

Ability to select "bank transfer" as the payment mean for a payment schedule

This commit is contained in:
Sylvain 2022-01-05 15:58:33 +01:00
parent d7ccbdbb52
commit 9922812111
19 changed files with 124 additions and 30 deletions

View File

@ -3,6 +3,7 @@
- Ability to cancel a payement schedule from the interface - Ability to cancel a payement schedule from the interface
- Ability to create slots in the past - Ability to create slots in the past
- Ability to disable public account creation - Ability to disable public account creation
- Ability to select "bank transfer" as the payment mean for a payment schedule
- Updated caniuse db - Updated caniuse db
- Optimized the load time of the payment schedules list - Optimized the load time of the payment schedules list
- Fix a bug: do not load Stripe if no keys were defined - Fix a bug: do not load Stripe if no keys were defined

View File

@ -4,7 +4,7 @@
class API::PaymentSchedulesController < API::ApiController class API::PaymentSchedulesController < API::ApiController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_payment_schedule, only: %i[download cancel] 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 # retrieve all payment schedules for the current user, paginated
def index def index
@ -46,6 +46,15 @@ class API::PaymentSchedulesController < API::ApiController
render json: attrs, status: :ok render json: attrs, status: :ok
end 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 def refresh_item
authorize @payment_schedule_item.payment_schedule authorize @payment_schedule_item.payment_schedule
PaymentScheduleItemWorker.new.perform(@payment_schedule_item.id) PaymentScheduleItemWorker.new.perform(@payment_schedule_item.id)

View File

@ -23,6 +23,11 @@ export default class PaymentScheduleAPI {
return res?.data; 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> { static async getItem (paymentScheduleItemId: number): Promise<PaymentScheduleItem> {
const res: AxiosResponse = await apiClient.get(`/api/payment_schedules/items/${paymentScheduleItemId}`); const res: AxiosResponse = await apiClient.get(`/api/payment_schedules/items/${paymentScheduleItemId}`);
return res?.data; return res?.data;

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
interface HtmlTranslateProps { interface HtmlTranslateProps {
trKey: string, trKey: string,
options?: Record<string, string> options?: Record<string, string|number>
} }
/** /**

View File

@ -31,6 +31,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
const [showExpanded, setShowExpanded] = useState<Map<number, boolean>>(new Map()); const [showExpanded, setShowExpanded] = useState<Map<number, boolean>>(new Map());
// is open, the modal dialog to confirm the cashing of a check? // is open, the modal dialog to confirm the cashing of a check?
const [showConfirmCashing, setShowConfirmCashing] = useState<boolean>(false); 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? // is open, the modal dialog the resolve a pending card payment?
const [showResolveAction, setShowResolveAction] = useState<boolean>(false); const [showResolveAction, setShowResolveAction] = useState<boolean>(false);
// the user cannot confirm the action modal (3D secure), unless he has resolved the pending action // 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. * Return the human-readable string for the status of the provided deadline.
*/ */
const formatState = (item: PaymentScheduleItem): JSX.Element => { const formatState = (item: PaymentScheduleItem, schedule: PaymentSchedule): JSX.Element => {
let res = t(`app.shared.schedules_table.state_${item.state}`); let res = t(`app.shared.schedules_table.state_${item.state}${item.state === 'pending' ? '_' + schedule.payment_method : ''}`);
if (item.state === PaymentScheduleItemState.Paid) { if (item.state === PaymentScheduleItemState.Paid) {
const key = `app.shared.schedules_table.method_${item.payment_method}`; const key = `app.shared.schedules_table.method_${item.payment_method}`;
res += ` (${t(key)})`; res += ` (${t(key)})`;
@ -155,12 +157,21 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
return downloadButton(TargetType.Invoice, item.invoice_id); return downloadButton(TargetType.Invoice, item.invoice_id);
case PaymentScheduleItemState.Pending: case PaymentScheduleItemState.Pending:
if (isPrivileged()) { if (isPrivileged()) {
if (schedule.payment_method === 'transfer') {
return ( return (
<FabButton onClick={handleConfirmCheckPayment(item)} <FabButton onClick={handleConfirmTransferPayment(item)}
icon={<i className="fas fa-money-check" />}> icon={<i className="fas fa-university"/>}>
{t('app.shared.schedules_table.confirm_payment')} {t('app.shared.schedules_table.confirm_payment')}
</FabButton> </FabButton>
); );
} else {
return (
<FabButton onClick={handleConfirmCheckPayment(item)}
icon={<i className="fas fa-money-check"/>}>
{t('app.shared.schedules_table.confirm_payment')}
</FabButton>
);
}
} else { } else {
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>; 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. * 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 * Refresh all payment schedules in the table
*/ */
@ -242,6 +274,13 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
setShowConfirmCashing(!showConfirmCashing); 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". * 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}> {_.orderBy(p.items, 'due_date').map(item => <tr key={item.id}>
<td>{FormatLib.date(item.due_date)}</td> <td>{FormatLib.date(item.due_date)}</td>
<td>{FormatLib.price(item.amount)}</td> <td>{FormatLib.price(item.amount)}</td>
<td>{formatState(item)}</td> <td>{formatState(item, p)}</td>
<td>{itemButtons(item, p)}</td> <td>{itemButtons(item, p)}</td>
</tr>)} </tr>)}
</tbody> </tbody>
@ -421,6 +460,20 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
})} })}
</span>} </span>}
</FabModal> </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 */} {/* Cancel the subscription */}
<FabModal title={t('app.shared.schedules_table.cancel_subscription')} <FabModal title={t('app.shared.schedules_table.cancel_subscription')}
isOpen={showCancelSubscription} isOpen={showCancelSubscription}

View File

@ -8,9 +8,9 @@ import SettingAPI from '../../../api/setting';
import { SettingName } from '../../../models/setting'; import { SettingName } from '../../../models/setting';
import { PaymentModal } from '../payment-modal'; import { PaymentModal } from '../payment-modal';
import { PaymentSchedule } from '../../../models/payment-schedule'; 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]; 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); const [onlinePaymentModal, setOnlinePaymentModal] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (cart.payment_method === PaymentMethod.Card) { setMethod(cart.payment_method || 'check');
setMethod('card');
} else {
setMethod('check');
}
}, [cart]); }, [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. * Callback triggered when the user selects a payment method for the current payment schedule.
*/ */
const handleUpdateMethod = (option: selectOption) => { const handleUpdateMethod = (option: selectOption) => {
if (option.value === 'card') { updateCart(Object.assign({}, cart, { payment_method: option.value }));
updateCart(Object.assign({}, cart, { payment_method: PaymentMethod.Card }));
} else {
updateCart(Object.assign({}, cart, { payment_method: PaymentMethod.Other }));
}
setMethod(option.value); setMethod(option.value);
}; };
@ -140,6 +132,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
value={methodToOption(method)} /> value={methodToOption(method)} />
{method === 'card' && <p>{t('app.admin.local_payment.card_collection_info')}</p>} {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 === '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>
<div className="full-schedule"> <div className="full-schedule">
<ul> <ul>

View File

@ -59,6 +59,7 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
case 'PayZen': case 'PayZen':
return renderPayZenModal(); return renderPayZenModal();
case '': case '':
case undefined:
return <div/>; return <div/>;
default: default:
onError(t('app.shared.update_card_modal.unexpected_error')); onError(t('app.shared.update_card_modal.unexpected_error'));

View File

@ -26,7 +26,7 @@ export interface PaymentSchedule {
id: number, id: number,
total: number, total: number,
reference: string, reference: string,
payment_method: 'card' | '', payment_method: 'card' | 'transfer' | '',
items: Array<PaymentScheduleItem>, items: Array<PaymentScheduleItem>,
created_at: Date, created_at: Date,
chained_footprint: boolean, chained_footprint: boolean,

View File

@ -18,7 +18,8 @@ export interface IntentConfirmation {
export enum PaymentMethod { export enum PaymentMethod {
Card = 'card', Card = 'card',
Other = '' Check = 'check',
Transfer = 'transfer'
} }
export type CartItem = { reservation: Reservation }| export type CartItem = { reservation: Reservation }|

View File

@ -58,6 +58,7 @@ class NotificationType
notify_admin_payment_schedule_failed notify_admin_payment_schedule_failed
notify_member_payment_schedule_failed notify_member_payment_schedule_failed
notify_admin_payment_schedule_check_deadline notify_admin_payment_schedule_check_deadline
notify_admin_payment_schedule_transfer_deadline
] ]
# deprecated: # deprecated:
# - notify_member_subscribed_plan_is_changed # - notify_member_subscribed_plan_is_changed

View File

@ -2,7 +2,7 @@
# Check the access policies for API::PaymentSchedulesController # Check the access policies for API::PaymentSchedulesController
class PaymentSchedulePolicy < ApplicationPolicy 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 define_method action do
user.admin? || user.manager? user.admin? || user.manager?
end end

View File

@ -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)

View File

@ -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>

View File

@ -22,8 +22,8 @@ class PaymentScheduleItemWorker
### Cards ### Cards
PaymentGatewayService.new.process_payment_schedule_item(psi) PaymentGatewayService.new.process_payment_schedule_item(psi)
elsif psi.state == 'new' elsif psi.state == 'new'
### Check (only new deadlines, to prevent spamming) ### Check/Bank transfer (only new deadlines, to prevent spamming)
NotificationCenter.call type: 'notify_admin_payment_schedule_check_deadline', NotificationCenter.call type: "notify_admin_payment_schedule_#{psi.payment_schedule.payment_method}_deadline",
receiver: User.admins_and_managers, receiver: User.admins_and_managers,
attached_object: psi attached_object: psi
psi.update_attributes(state: 'pending') psi.update_attributes(state: 'pending')

View File

@ -1451,8 +1451,10 @@ en:
payment_method: "Payment method" payment_method: "Payment method"
method_card: "Online by card" method_card: "Online by card"
method_check: "By check" 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." 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." 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." online_payment_disabled: "Online payment is not available. You cannot collect this payment schedule by online card."
check_list_setting: check_list_setting:
save: 'Save' save: 'Save'

View File

@ -487,7 +487,8 @@ en:
state: "State" state: "State"
download: "Download" download: "Download"
state_new: "Not yet due" 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_payment_method: "The credit card must be updated"
state_requires_action: "Action required" state_requires_action: "Action required"
state_paid: "Paid" state_paid: "Paid"
@ -495,11 +496,14 @@ en:
state_canceled: "Canceled" state_canceled: "Canceled"
method_card: "by card" method_card: "by card"
method_check: "by check" method_check: "by check"
method_transfer: "by transfer"
confirm_payment: "Confirm payment" confirm_payment: "Confirm payment"
solve: "Solve" solve: "Solve"
update_card: "Update the card" update_card: "Update the card"
confirm_check_cashing: "Confirm the cashing of the check" 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_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" confirm_button: "Confirm"
resolve_action: "Resolve the action" resolve_action: "Resolve the action"
ok_button: "OK" ok_button: "OK"

View File

@ -126,7 +126,7 @@ en:
deadline_date: "Payment date" deadline_date: "Payment date"
deadline_amount: "Amount including tax" deadline_amount: "Amount including tax"
total_amount: "Total amount" 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." settlement_by_wallet: "%{AMOUNT} will be debited from your wallet to settle the first deadline."
# CVS accounting export (columns headers) # CVS accounting export (columns headers)
accounting_export: accounting_export:
@ -373,6 +373,8 @@ en:
schedule_failed: "Failed card debit for the %{DATE} deadline, for your schedule %{REFERENCE}" schedule_failed: "Failed card debit for the %{DATE} deadline, for your schedule %{REFERENCE}"
notify_admin_payment_schedule_check_deadline: notify_admin_payment_schedule_check_deadline:
schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}" 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 tools for admins
statistics: statistics:
subscriptions: "Subscriptions" subscriptions: "Subscriptions"

View File

@ -315,5 +315,11 @@ en:
remember: "In accordance with the %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}." 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." 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." 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: shared:
hello: "Hello %{user_name}" hello: "Hello %{user_name}"

View File

@ -125,6 +125,7 @@ Rails.application.routes.draw do
get 'download', on: :member get 'download', on: :member
get 'items/:id', action: 'show_item', on: :collection get 'items/:id', action: 'show_item', on: :collection
post 'items/:id/cash_check', action: 'cash_check', 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/refresh_item', action: 'refresh_item', on: :collection
post 'items/:id/pay_item', action: 'pay_item', on: :collection post 'items/:id/pay_item', action: 'pay_item', on: :collection
end end