From 992281211192a9df742bdc649ac2a1bc54862936 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 5 Jan 2022 15:58:33 +0100 Subject: [PATCH] Ability to select "bank transfer" as the payment mean for a payment schedule --- CHANGELOG.md | 1 + .../api/payment_schedules_controller.rb | 11 ++- .../src/javascript/api/payment-schedule.ts | 5 ++ .../components/base/html-translate.tsx | 2 +- .../payment-schedules-table.tsx | 71 ++++++++++++++++--- .../local-payment/local-payment-form.tsx | 17 ++--- .../components/payment/update-card-modal.tsx | 1 + .../src/javascript/models/payment-schedule.ts | 2 +- app/frontend/src/javascript/models/payment.ts | 3 +- app/models/notification_type.rb | 1 + app/policies/payment_schedule_policy.rb | 2 +- ...yment_schedule_bank_deadline.json.jbuilder | 5 ++ ...in_payment_schedule_bank_deadline.html.erb | 10 +++ app/workers/payment_schedule_item_worker.rb | 4 +- config/locales/app.admin.en.yml | 2 + config/locales/app.shared.en.yml | 6 +- config/locales/en.yml | 4 +- config/locales/mails.en.yml | 6 ++ config/routes.rb | 1 + 19 files changed, 124 insertions(+), 30 deletions(-) create mode 100644 app/views/api/notifications/_notify_admin_payment_schedule_bank_deadline.json.jbuilder create mode 100644 app/views/notifications_mailer/notify_admin_payment_schedule_bank_deadline.html.erb diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d18adcf..18aff1040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/controllers/api/payment_schedules_controller.rb b/app/controllers/api/payment_schedules_controller.rb index 561490db0..6b2050ed0 100644 --- a/app/controllers/api/payment_schedules_controller.rb +++ b/app/controllers/api/payment_schedules_controller.rb @@ -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) diff --git a/app/frontend/src/javascript/api/payment-schedule.ts b/app/frontend/src/javascript/api/payment-schedule.ts index 8e8167a06..410f09681 100644 --- a/app/frontend/src/javascript/api/payment-schedule.ts +++ b/app/frontend/src/javascript/api/payment-schedule.ts @@ -23,6 +23,11 @@ export default class PaymentScheduleAPI { return res?.data; } + static async confirmTransfer (paymentScheduleItemId: number): Promise { + const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/confirm_transfer`); + return res?.data; + } + static async getItem (paymentScheduleItemId: number): Promise { const res: AxiosResponse = await apiClient.get(`/api/payment_schedules/items/${paymentScheduleItemId}`); return res?.data; diff --git a/app/frontend/src/javascript/components/base/html-translate.tsx b/app/frontend/src/javascript/components/base/html-translate.tsx index 68abc8b52..9b0c190cd 100644 --- a/app/frontend/src/javascript/components/base/html-translate.tsx +++ b/app/frontend/src/javascript/components/base/html-translate.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; interface HtmlTranslateProps { trKey: string, - options?: Record + options?: Record } /** diff --git a/app/frontend/src/javascript/components/payment-schedule/payment-schedules-table.tsx b/app/frontend/src/javascript/components/payment-schedule/payment-schedules-table.tsx index cd6632632..b767d2bce 100644 --- a/app/frontend/src/javascript/components/payment-schedule/payment-schedules-table.tsx +++ b/app/frontend/src/javascript/components/payment-schedule/payment-schedules-table.tsx @@ -31,6 +31,8 @@ const PaymentSchedulesTableComponent: React.FC = ({ const [showExpanded, setShowExpanded] = useState>(new Map()); // is open, the modal dialog to confirm the cashing of a check? const [showConfirmCashing, setShowConfirmCashing] = useState(false); + // is open, the modal dialog to confirm a back transfer? + const [showConfirmTransfer, setShowConfirmTransfer] = useState(false); // is open, the modal dialog the resolve a pending card payment? const [showResolveAction, setShowResolveAction] = useState(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 = ({ /** * 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 = ({ return downloadButton(TargetType.Invoice, item.invoice_id); case PaymentScheduleItemState.Pending: if (isPrivileged()) { - return ( - }> - {t('app.shared.schedules_table.confirm_payment')} - - ); + if (schedule.payment_method === 'transfer') { + return ( + }> + {t('app.shared.schedules_table.confirm_payment')} + + ); + } else { + return ( + }> + {t('app.shared.schedules_table.confirm_payment')} + + ); + } } else { return {t('app.shared.schedules_table.please_ask_reception')}; } @@ -216,6 +227,15 @@ const PaymentSchedulesTableComponent: React.FC = ({ }; }; + /** + * 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 = ({ }); }; + /** + * 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 = ({ 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 = ({ {_.orderBy(p.items, 'due_date').map(item => {FormatLib.date(item.due_date)} {FormatLib.price(item.amount)} - {formatState(item)} + {formatState(item, p)} {itemButtons(item, p)} )} @@ -421,6 +460,20 @@ const PaymentSchedulesTableComponent: React.FC = ({ })} } + {/* Confirm the bank transfer for the current deadline */} + + {tempDeadline && + {t('app.shared.schedules_table.confirm_bank_transfer_body', { + AMOUNT: FormatLib.price(tempDeadline.amount), + DATE: FormatLib.date(tempDeadline.due_date) + })} + } + {/* Cancel the subscription */} = ({ onSubmit, onSucce const [onlinePaymentModal, setOnlinePaymentModal] = useState(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 = ({ 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 = ({ onSubmit, onSucce value={methodToOption(method)} /> {method === 'card' &&

{t('app.admin.local_payment.card_collection_info')}

} {method === 'check' &&

{t('app.admin.local_payment.check_collection_info', { DEADLINES: paymentSchedule.items.length })}

} + {method === 'transfer' && }
    diff --git a/app/frontend/src/javascript/components/payment/update-card-modal.tsx b/app/frontend/src/javascript/components/payment/update-card-modal.tsx index 9b83e9866..a57093dee 100644 --- a/app/frontend/src/javascript/components/payment/update-card-modal.tsx +++ b/app/frontend/src/javascript/components/payment/update-card-modal.tsx @@ -59,6 +59,7 @@ const UpdateCardModalComponent: React.FC = ({ isOpen, togg case 'PayZen': return renderPayZenModal(); case '': + case undefined: return
    ; default: onError(t('app.shared.update_card_modal.unexpected_error')); diff --git a/app/frontend/src/javascript/models/payment-schedule.ts b/app/frontend/src/javascript/models/payment-schedule.ts index 176d5fb98..3a637ecc6 100644 --- a/app/frontend/src/javascript/models/payment-schedule.ts +++ b/app/frontend/src/javascript/models/payment-schedule.ts @@ -26,7 +26,7 @@ export interface PaymentSchedule { id: number, total: number, reference: string, - payment_method: 'card' | '', + payment_method: 'card' | 'transfer' | '', items: Array, created_at: Date, chained_footprint: boolean, diff --git a/app/frontend/src/javascript/models/payment.ts b/app/frontend/src/javascript/models/payment.ts index b59440617..8212b6aaf 100644 --- a/app/frontend/src/javascript/models/payment.ts +++ b/app/frontend/src/javascript/models/payment.ts @@ -18,7 +18,8 @@ export interface IntentConfirmation { export enum PaymentMethod { Card = 'card', - Other = '' + Check = 'check', + Transfer = 'transfer' } export type CartItem = { reservation: Reservation }| diff --git a/app/models/notification_type.rb b/app/models/notification_type.rb index 638bee6a4..394a12424 100644 --- a/app/models/notification_type.rb +++ b/app/models/notification_type.rb @@ -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 diff --git a/app/policies/payment_schedule_policy.rb b/app/policies/payment_schedule_policy.rb index 633c03eb8..866892c40 100644 --- a/app/policies/payment_schedule_policy.rb +++ b/app/policies/payment_schedule_policy.rb @@ -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 diff --git a/app/views/api/notifications/_notify_admin_payment_schedule_bank_deadline.json.jbuilder b/app/views/api/notifications/_notify_admin_payment_schedule_bank_deadline.json.jbuilder new file mode 100644 index 000000000..9f9575d88 --- /dev/null +++ b/app/views/api/notifications/_notify_admin_payment_schedule_bank_deadline.json.jbuilder @@ -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) diff --git a/app/views/notifications_mailer/notify_admin_payment_schedule_bank_deadline.html.erb b/app/views/notifications_mailer/notify_admin_payment_schedule_bank_deadline.html.erb new file mode 100644 index 000000000..5cdc9b065 --- /dev/null +++ b/app/views/notifications_mailer/notify_admin_payment_schedule_bank_deadline.html.erb @@ -0,0 +1,10 @@ +<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> + +

    + <%= 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') %> +

    +

    <%= t('.body.confirm') %>

    diff --git a/app/workers/payment_schedule_item_worker.rb b/app/workers/payment_schedule_item_worker.rb index 9f9b6ec0b..6579d1f46 100644 --- a/app/workers/payment_schedule_item_worker.rb +++ b/app/workers/payment_schedule_item_worker.rb @@ -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') diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index d7d19070c..94d63d713 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -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: "

    By validating, you confirm that you set up {DEADLINES} bank direct debits, allowing you to collect all the monthly payments.

    Please note: the bank transfers are not automatically handled by Fab-manager.

    " online_payment_disabled: "Online payment is not available. You cannot collect this payment schedule by online card." check_list_setting: save: 'Save' diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 0e23b125a..2cadef5ad 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -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" diff --git a/config/locales/en.yml b/config/locales/en.yml index ab0f11318..35929bd27 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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" diff --git a/config/locales/mails.en.yml b/config/locales/mails.en.yml index f4d6d3f54..15ee9d53f 100644 --- a/config/locales/mails.en.yml +++ b/config/locales/mails.en.yml @@ -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}" diff --git a/config/routes.rb b/config/routes.rb index 3f588facd..bdbd3cff2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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