1
0
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:
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 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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
interface HtmlTranslateProps {
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());
// 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}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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