mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-17 06:52:27 +01:00
Ability to cancel a payement schedule from the interface
This commit is contained in:
parent
effe5c7ba9
commit
f3f15a2b9d
@ -1,5 +1,6 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
- Ability to cancel a payement schedule from the interface
|
||||
- Updated caniuse db
|
||||
- Optimized the load time of the payment schedules list
|
||||
|
||||
|
@ -2,5 +2,8 @@
|
||||
|
||||
# Raised when an an error occurred with the PayZen payment gateway
|
||||
class PayzenError < PaymentGatewayError
|
||||
def details
|
||||
JSON.parse(message.gsub('=>', ':').gsub('nil', 'null'))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { ReactEventHandler, useState } from 'react';
|
||||
import React, { ReactElement, ReactEventHandler, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader } from '../base/loader';
|
||||
import _ from 'lodash';
|
||||
@ -6,7 +6,6 @@ import { FabButton } from '../base/fab-button';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { UpdateCardModal } from '../payment/update-card-modal';
|
||||
import { StripeElements } from '../payment/stripe/stripe-elements';
|
||||
import { StripeConfirm } from '../payment/stripe/stripe-confirm';
|
||||
import { User, UserRole } from '../../models/user';
|
||||
import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '../../models/payment-schedule';
|
||||
import PaymentScheduleAPI from '../../api/payment-schedule';
|
||||
@ -47,6 +46,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
|
||||
// we want to display the card update button, only once. This is an association table keeping when we already shown one
|
||||
const cardUpdateButton = new Map<number, boolean>();
|
||||
// we want to display the cancel subscription button, only once. This is an association table keeping when we already shown one
|
||||
const subscriptionCancelButton = new Map<number, boolean>();
|
||||
|
||||
/**
|
||||
* Check if the requested payment schedule is displayed with its deadlines (PaymentScheduleItem) or without them
|
||||
@ -111,6 +112,21 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a button to cancel the given subscription, if the user is privileged enough
|
||||
*/
|
||||
const cancelSubscriptionButton = (schedule: PaymentSchedule): ReactElement => {
|
||||
if (isPrivileged() && !subscriptionCancelButton.get(schedule.id)) {
|
||||
subscriptionCancelButton.set(schedule.id, true);
|
||||
return (
|
||||
<FabButton onClick={handleCancelSubscription(schedule)}
|
||||
icon={<i className="fas fa-times" />}>
|
||||
{t('app.shared.schedules_table.cancel_subscription')}
|
||||
</FabButton>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the human-readable string for the status of the provided deadline.
|
||||
*/
|
||||
@ -165,25 +181,24 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
case PaymentScheduleItemState.Error:
|
||||
// if the payment is in error, the schedule is over, and we can't update the card
|
||||
cardUpdateButton.set(schedule.id, true);
|
||||
if (isPrivileged()) {
|
||||
return (
|
||||
<FabButton onClick={handleCancelSubscription(schedule)}
|
||||
icon={<i className="fas fa-times" />}>
|
||||
{t('app.shared.schedules_table.cancel_subscription')}
|
||||
</FabButton>
|
||||
);
|
||||
} else {
|
||||
if (!isPrivileged()) {
|
||||
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>;
|
||||
}
|
||||
return cancelSubscriptionButton(schedule);
|
||||
case PaymentScheduleItemState.New:
|
||||
if (!cardUpdateButton.get(schedule.id)) {
|
||||
if (!cardUpdateButton.get(schedule.id) && schedule.payment_method === 'card') {
|
||||
cardUpdateButton.set(schedule.id, true);
|
||||
return (
|
||||
<FabButton onClick={handleUpdateCard(schedule)}
|
||||
icon={<i className="fas fa-credit-card" />}>
|
||||
{t('app.shared.schedules_table.update_card')}
|
||||
</FabButton>
|
||||
<span>
|
||||
<FabButton onClick={handleUpdateCard(schedule)}
|
||||
icon={<i className="fas fa-credit-card" />}>
|
||||
{t('app.shared.schedules_table.update_card')}
|
||||
</FabButton>
|
||||
{cancelSubscriptionButton(schedule)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return cancelSubscriptionButton(schedule);
|
||||
}
|
||||
return <span />;
|
||||
default:
|
||||
@ -263,7 +278,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user's clicks on the "update card" button: show a modal to input a new card
|
||||
* Callback triggered when the user's clicks on the "update card" button: show a modal to input a new card.
|
||||
* If a payementScheduleItem is provided, the payment will be triggered after a successful update (see handleCardUpdateSuccess).
|
||||
*/
|
||||
const handleUpdateCard = (paymentSchedule: PaymentSchedule, item?: PaymentScheduleItem): ReactEventHandler => {
|
||||
return (): void => {
|
||||
@ -391,6 +407,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="modals">
|
||||
{/* Confirm the cashing of the current deadline by check */}
|
||||
<FabModal title={t('app.shared.schedules_table.confirm_check_cashing')}
|
||||
isOpen={showConfirmCashing}
|
||||
toggleModal={toggleConfirmCashingModal}
|
||||
@ -404,6 +421,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
})}
|
||||
</span>}
|
||||
</FabModal>
|
||||
{/* Cancel the subscription */}
|
||||
<FabModal title={t('app.shared.schedules_table.cancel_subscription')}
|
||||
isOpen={showCancelSubscription}
|
||||
toggleModal={toggleCancelSubscriptionModal}
|
||||
@ -413,10 +431,12 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
{t('app.shared.schedules_table.confirm_cancel_subscription')}
|
||||
</FabModal>
|
||||
<StripeElements>
|
||||
{/* 3D secure confirmation */}
|
||||
{tempDeadline && <StripeConfirmModal isOpen={showResolveAction}
|
||||
toggleModal={toggleResolveActionModal}
|
||||
onSuccess={afterAction}
|
||||
paymentScheduleItemId={tempDeadline.id} />}
|
||||
{/* Update credit card */}
|
||||
{tempSchedule && <UpdateCardModal isOpen={showUpdateCard}
|
||||
toggleModal={toggleUpdateCardModal}
|
||||
operator={operator}
|
||||
|
@ -24,11 +24,7 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
|
||||
const [gateway, setGateway] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (schedule.gateway_subscription.classname.match(/^PayZen::/)) {
|
||||
setGateway('payzen');
|
||||
} else if (schedule.gateway_subscription.classname.match(/^Stripe::/)) {
|
||||
setGateway('stripe');
|
||||
}
|
||||
setGateway(schedule.gateway);
|
||||
}, [schedule]);
|
||||
|
||||
/**
|
||||
@ -44,7 +40,7 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
|
||||
|
||||
/**
|
||||
* Render the PayZen update-card modal
|
||||
*/ // 1
|
||||
*/
|
||||
const renderPayZenModal = (): ReactElement => {
|
||||
return <PayzenCardUpdateModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
@ -58,15 +54,15 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
|
||||
*/
|
||||
|
||||
switch (gateway) {
|
||||
case 'stripe':
|
||||
case 'Stripe':
|
||||
return renderStripeModal();
|
||||
case 'payzen':
|
||||
case 'PayZen':
|
||||
return renderPayZenModal();
|
||||
case '':
|
||||
return <div/>;
|
||||
default:
|
||||
onError(t('app.shared.update_card_modal.unexpected_error'));
|
||||
console.error(`[UpdateCardModal] unexpected gateway: ${schedule.gateway_subscription?.classname}`);
|
||||
console.error(`[UpdateCardModal] unexpected gateway: ${schedule.gateway} for schedule ${schedule.id}`);
|
||||
return <div />;
|
||||
}
|
||||
};
|
||||
|
@ -26,7 +26,7 @@ export interface PaymentSchedule {
|
||||
id: number,
|
||||
total: number,
|
||||
reference: string,
|
||||
payment_method: string,
|
||||
payment_method: 'card' | '',
|
||||
items: Array<PaymentScheduleItem>,
|
||||
created_at: Date,
|
||||
chained_footprint: boolean,
|
||||
@ -43,9 +43,7 @@ export interface PaymentSchedule {
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
},
|
||||
gateway_subscription: {
|
||||
classname: string
|
||||
}
|
||||
gateway: 'PayZen' | 'Stripe',
|
||||
}
|
||||
|
||||
export interface PaymentScheduleIndexRequest {
|
||||
|
@ -23,6 +23,11 @@ class PaymentGatewayService
|
||||
@gateway.create_subscription(payment_schedule, *args)
|
||||
end
|
||||
|
||||
def cancel_subscription(payment_schedule)
|
||||
gateway = service_for_payment_schedule(payment_schedule)
|
||||
gateway.cancel_subscription(payment_schedule)
|
||||
end
|
||||
|
||||
def create_user(user_id)
|
||||
@gateway.create_user(user_id)
|
||||
end
|
||||
@ -52,7 +57,7 @@ class PaymentGatewayService
|
||||
private
|
||||
|
||||
def service_for_payment_schedule(payment_schedule)
|
||||
service = case payment_schedule.gateway_subscription.klass
|
||||
service = case payment_schedule.gateway_subscription&.klass
|
||||
when /^PayZen::/
|
||||
require 'pay_zen/service'
|
||||
PayZen::Service
|
||||
|
@ -158,6 +158,8 @@ class PaymentScheduleService
|
||||
end
|
||||
|
||||
def self.cancel(payment_schedule)
|
||||
PaymentGatewayService.new.cancel_subscription(payment_schedule)
|
||||
|
||||
# cancel all item where state != paid
|
||||
payment_schedule.ordered_items.each do |item|
|
||||
next if item.state == 'paid'
|
||||
|
@ -18,10 +18,8 @@ json.main_object do
|
||||
json.id payment_schedule.main_object.object_id
|
||||
end
|
||||
if payment_schedule.gateway_subscription
|
||||
json.gateway_subscription do
|
||||
# this attribute is used to known which gateway should we interact with, in the front-end
|
||||
json.classname payment_schedule.gateway_subscription.klass
|
||||
end
|
||||
# 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
|
||||
end
|
||||
json.items payment_schedule.payment_schedule_items do |item|
|
||||
json.partial! 'api/payment_schedules/payment_schedule_item', item: item
|
||||
|
@ -48,6 +48,35 @@ class PayZen::Service < Payment::Service
|
||||
pgo_sub.save!
|
||||
end
|
||||
|
||||
def cancel_subscription(payment_schedule)
|
||||
pz_subscription = payment_schedule.gateway_subscription.retrieve
|
||||
|
||||
order_client = PayZen::Order.new
|
||||
tr_client = PayZen::Transaction.new
|
||||
|
||||
# first, we cancel all running transactions
|
||||
begin
|
||||
order = order_client.get(pz_subscription['answer']['orderId'])
|
||||
order['answer']['transactions'].select { |t| t['status'] == 'RUNNING' }.each do |t|
|
||||
tr_res = tr_client.cancel_or_refund(t['uuid'], amount: t['amount'], currency: t['currency'], resolution_mode: 'CANCELLATION_ONLY')
|
||||
raise "Cannot cancel transaction #{t['uuid']}" unless tr_res['answer']['detailedStatus'] == 'CANCELLED'
|
||||
end
|
||||
rescue PayzenError => e
|
||||
raise e unless e.details['errorCode'] == 'PSP_010' # ignore if no order
|
||||
end
|
||||
|
||||
# then, we cancel the subscription
|
||||
begin
|
||||
sub_client = PayZen::Subscription.new
|
||||
res = sub_client.cancel(pz_subscription['answer']['subscriptionId'], pz_subscription['answer']['paymentMethodToken'])
|
||||
rescue PayzenError => e
|
||||
return true if e.details['errorCode'] == 'PSP_033' # recurring payment already canceled
|
||||
|
||||
raise e
|
||||
end
|
||||
res['answer']['responseCode'].zero?
|
||||
end
|
||||
|
||||
def process_payment_schedule_item(payment_schedule_item)
|
||||
pz_order = payment_schedule_item.payment_schedule.gateway_order.retrieve
|
||||
transaction = pz_order['answer']['transactions'].last
|
||||
|
@ -14,4 +14,11 @@ class PayZen::Subscription < PayZen::Client
|
||||
def get(subscription_id, payment_method_token)
|
||||
post('/Subscription/Get/', subscriptionId: subscription_id, paymentMethodToken: payment_method_token)
|
||||
end
|
||||
|
||||
##
|
||||
# @see https://payzen.io/fr-FR/rest/V4.0/api/playground/Subscription/Cancel/
|
||||
##
|
||||
def cancel(subscription_id, payment_method_token)
|
||||
post('/Subscription/Cancel/', subscriptionId: subscription_id, paymentMethodToken: payment_method_token)
|
||||
end
|
||||
end
|
||||
|
@ -14,4 +14,15 @@ class PayZen::Transaction < PayZen::Client
|
||||
def get(uuid)
|
||||
post('/Transaction/Get/', uuid: uuid)
|
||||
end
|
||||
|
||||
##
|
||||
# @see https://payzen.io/fr-FR/rest/V4.0/api/playground/Transaction/CancelOrRefund
|
||||
##
|
||||
def cancel_or_refund(uuid,
|
||||
amount: 0,
|
||||
currency: Setting.get('payzen_currency'),
|
||||
resolution_mode: nil,
|
||||
comment: nil)
|
||||
post('/Transaction/CancelOrRefund/', uuid: uuid, amount: amount, currency: currency, resolutionMode: resolution_mode, comment: comment)
|
||||
end
|
||||
end
|
||||
|
@ -8,6 +8,8 @@ module Payment; end
|
||||
class Payment::Service
|
||||
def create_subscription(_payment_schedule, *args); end
|
||||
|
||||
def cancel_subscription(_payment_schedule); end
|
||||
|
||||
def create_user(_user_id); end
|
||||
|
||||
def create_coupon(_coupon_id); end
|
||||
|
@ -43,6 +43,15 @@ class Stripe::Service < Payment::Service
|
||||
pgo.save!
|
||||
end
|
||||
|
||||
def cancel_subscription(payment_schedule)
|
||||
stripe_key = Setting.get('stripe_secret_key')
|
||||
|
||||
stp_subscription = payment_schedule.gateway_subscription.retrieve
|
||||
|
||||
res = Stripe::Subscription.delete(stp_subscription.id, {}, api_key: stripe_key)
|
||||
res.status == 'canceled'
|
||||
end
|
||||
|
||||
def create_user(user_id)
|
||||
StripeWorker.perform_async(:create_stripe_customer, user_id)
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user