mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-29 18:52:22 +01:00
fix renew subscription API
TODO: - remove old endpoints - bug in UI (start date invalid) - fix tests - payzen
This commit is contained in:
parent
3349cc3a2d
commit
fd39eaf2f1
@ -84,8 +84,13 @@ class API::StripeController < API::PaymentsController
|
||||
|
||||
stp_subscription = service.subscribe(method.id, cart)
|
||||
|
||||
res = on_payment_success(stp_subscription, cart) if stp_subscription&.status == 'active'
|
||||
render generate_payment_response(stp_subscription.latest_invoice.payment_intent, 'subscription', res, stp_subscription.id)
|
||||
res = on_payment_success(stp_subscription, cart) if %w[active not_started].include?(stp_subscription&.status)
|
||||
intent = if stp_subscription.object == 'subscription_schedule'
|
||||
nil
|
||||
elsif stp_subscription.object == 'subscription'
|
||||
stp_subscription.latest_invoice&.payment_intent
|
||||
end
|
||||
render generate_payment_response(intent, 'subscription', res, stp_subscription.id)
|
||||
end
|
||||
|
||||
def confirm_subscription
|
||||
|
@ -27,6 +27,7 @@ export interface GatewayFormProps {
|
||||
className?: string,
|
||||
paymentSchedule?: PaymentSchedule,
|
||||
cart?: ShoppingCart,
|
||||
updateCart?: (cart: ShoppingCart) => void,
|
||||
formId: string,
|
||||
}
|
||||
|
||||
@ -36,6 +37,7 @@ interface AbstractPaymentModalProps {
|
||||
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
||||
onError: (message: string) => void,
|
||||
cart: ShoppingCart,
|
||||
updateCart?: (cart: ShoppingCart) => void,
|
||||
currentUser: User,
|
||||
schedule?: PaymentSchedule,
|
||||
customer: User,
|
||||
@ -56,7 +58,7 @@ interface AbstractPaymentModalProps {
|
||||
* This component must not be called directly but must be extended for each implemented payment gateway
|
||||
* @see https://reactjs.org/docs/composition-vs-inheritance.html
|
||||
*/
|
||||
export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, currentUser, schedule, customer, logoFooter, GatewayForm, formId, className, formClassName, title, preventCgv, preventScheduleInfo, modalSize }) => {
|
||||
export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, updateCart, currentUser, schedule, customer, logoFooter, GatewayForm, formId, className, formClassName, title, preventCgv, preventScheduleInfo, modalSize }) => {
|
||||
// customer's wallet
|
||||
const [wallet, setWallet] = useState<Wallet>(null);
|
||||
// server-computed price with all details
|
||||
@ -205,6 +207,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
|
||||
className={`gateway-form ${formClassName || ''}`}
|
||||
formId={formId}
|
||||
cart={cart}
|
||||
updateCart={updateCart}
|
||||
customer={customer}
|
||||
paymentSchedule={schedule}>
|
||||
{hasErrors() && <div className="payment-errors">
|
||||
|
@ -24,7 +24,7 @@ type selectOption = { value: scheduleMethod, label: string };
|
||||
* This is intended for use by privileged users.
|
||||
* The form validation button must be created elsewhere, using the attribute form={formId}.
|
||||
*/
|
||||
export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule, cart, customer, operator, formId }) => {
|
||||
export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule, cart, updateCart, customer, operator, formId }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [method, setMethod] = useState<scheduleMethod>('check');
|
||||
@ -58,9 +58,9 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
*/
|
||||
const handleUpdateMethod = (option: selectOption) => {
|
||||
if (option.value === 'card') {
|
||||
cart.payment_method = PaymentMethod.Card;
|
||||
updateCart(Object.assign({}, cart, { payment_method: PaymentMethod.Card }));
|
||||
} else {
|
||||
cart.payment_method = PaymentMethod.Other;
|
||||
updateCart(Object.assign({}, cart, { payment_method: PaymentMethod.Other }));
|
||||
}
|
||||
setMethod(option.value);
|
||||
};
|
||||
|
@ -19,6 +19,7 @@ interface LocalPaymentModalProps {
|
||||
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
||||
onError: (message: string) => void,
|
||||
cart: ShoppingCart,
|
||||
updateCart: (cart: ShoppingCart) => void,
|
||||
currentUser: User,
|
||||
schedule?: PaymentSchedule,
|
||||
customer: User
|
||||
@ -27,7 +28,7 @@ interface LocalPaymentModalProps {
|
||||
/**
|
||||
* This component enables a privileged user to confirm a local payments.
|
||||
*/
|
||||
const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, currentUser, schedule, customer }) => {
|
||||
const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, updateCart, currentUser, schedule, customer }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
/**
|
||||
@ -44,7 +45,7 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
|
||||
/**
|
||||
* Integrates the LocalPaymentForm into the parent AbstractPaymentModal
|
||||
*/
|
||||
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, customer, paymentSchedule, children }) => {
|
||||
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, updateCart, customer, paymentSchedule, children }) => {
|
||||
return (
|
||||
<LocalPaymentForm onSubmit={onSubmit}
|
||||
onSuccess={onSuccess}
|
||||
@ -53,6 +54,7 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
|
||||
className={className}
|
||||
formId={formId}
|
||||
cart={cart}
|
||||
updateCart={updateCart}
|
||||
customer={customer}
|
||||
paymentSchedule={paymentSchedule}>
|
||||
{children}
|
||||
@ -70,6 +72,7 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
|
||||
formClassName="local-payment-form"
|
||||
currentUser={currentUser}
|
||||
cart={cart}
|
||||
updateCart={updateCart}
|
||||
customer={customer}
|
||||
afterSuccess={afterSuccess}
|
||||
onError={onError}
|
||||
@ -81,12 +84,12 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
|
||||
);
|
||||
};
|
||||
|
||||
export const LocalPaymentModal: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
|
||||
export const LocalPaymentModal: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, updateCart, customer }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<LocalPaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} />
|
||||
<LocalPaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} updateCart={updateCart} customer={customer} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('localPaymentModal', react2angular(LocalPaymentModal, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'customer']));
|
||||
Application.Components.component('localPaymentModal', react2angular(LocalPaymentModal, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'updateCart', 'customer']));
|
||||
|
@ -164,6 +164,7 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
|
||||
afterSuccess={handlePackBought}
|
||||
onError={onError}
|
||||
cart={cart}
|
||||
updateCart={setCart}
|
||||
currentUser={operator}
|
||||
customer={customer} />
|
||||
</div>}
|
||||
|
@ -132,7 +132,7 @@ const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscripti
|
||||
readOnly/>
|
||||
</form>
|
||||
<div className="payment">
|
||||
<SelectSchedule show selected={scheduleRequired} onChange={setScheduleRequired} />
|
||||
{subscription.plan.monthly_payment && <SelectSchedule show selected={scheduleRequired} onChange={setScheduleRequired} />}
|
||||
{price?.schedule && <PaymentScheduleSummary schedule={price.schedule as PaymentSchedule} />}
|
||||
{price && !price?.schedule && <div className="one-go-payment">
|
||||
<h4>{t('app.admin.renew_subscription_modal.pay_in_one_go')}</h4>
|
||||
@ -145,6 +145,7 @@ const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscripti
|
||||
afterSuccess={onPaymentSuccess}
|
||||
onError={onError}
|
||||
cart={cart}
|
||||
updateCart={setCart}
|
||||
currentUser={operator}
|
||||
customer={customer}
|
||||
schedule={price?.schedule as PaymentSchedule} />
|
||||
|
@ -378,6 +378,17 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when a child component (LocalPaymentModal) requires to update the cart content
|
||||
* @param cart {ShoppingCart}
|
||||
*/
|
||||
$scope.updateCart = (cart) => {
|
||||
setTimeout(() => {
|
||||
$scope.localPayment.cartItems = cart;
|
||||
$scope.$apply();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
|
@ -216,6 +216,7 @@
|
||||
after-success="afterLocalPaymentSuccess"
|
||||
on-error="onLocalPaymentError"
|
||||
cart="localPayment.cartItems"
|
||||
update-cart="updateCart"
|
||||
current-user="currentUser"
|
||||
customer="user"
|
||||
schedule="schedule.payment_schedule"/>
|
||||
|
@ -130,7 +130,7 @@ class Invoice < PaymentDocument
|
||||
return true if user.nil?
|
||||
|
||||
# workaround for reservation saved after invoice
|
||||
if main_item&.object_type == 'Reservation' && main_item&.object&.reservable_type == 'Training'
|
||||
if main_item.object_type == 'Reservation' && main_item.object&.reservable_type == 'Training'
|
||||
user.trainings.include?(main_item.object.reservable_id)
|
||||
else
|
||||
false
|
||||
|
@ -22,7 +22,7 @@ class PaymentDocument < Footprintable
|
||||
self.wallet_transaction_id = transaction_id
|
||||
end
|
||||
|
||||
def post_save(arg); end
|
||||
def post_save(*args); end
|
||||
|
||||
def render_resource; end
|
||||
end
|
||||
|
@ -75,10 +75,10 @@ class PaymentSchedule < PaymentDocument
|
||||
payment_schedule_items
|
||||
end
|
||||
|
||||
def post_save(gateway_method_id)
|
||||
def post_save(*args)
|
||||
return unless payment_method == 'card'
|
||||
|
||||
PaymentGatewayService.new.create_subscription(self, gateway_method_id)
|
||||
PaymentGatewayService.new.create_subscription(self, *args)
|
||||
end
|
||||
|
||||
def post_save_extend(gateway_method_id)
|
||||
|
@ -61,7 +61,7 @@ class ShoppingCart
|
||||
payment = create_payment_document(price, objects, payment_id, payment_type)
|
||||
WalletService.debit_user_wallet(payment, @customer)
|
||||
payment.save
|
||||
payment.post_save(payment_id)
|
||||
payment.post_save(payment_id, payment_type)
|
||||
end
|
||||
|
||||
success = !payment.nil? && objects.map(&:errors).flatten.map(&:empty?).all? && items.map(&:errors).map(&:empty?).all?
|
||||
|
@ -17,7 +17,7 @@ class CartService
|
||||
items = []
|
||||
cart_items[:items].each do |item|
|
||||
if ['subscription', :subscription].include?(item.keys.first)
|
||||
items.push(CartItem::Subscription.new(plan_info[:plan], @customer)) if plan_info[:new_subscription]
|
||||
items.push(CartItem::Subscription.new(plan_info[:plan], @customer, item[:subscription][:start_at])) if plan_info[:new_subscription]
|
||||
elsif ['reservation', :reservation].include?(item.keys.first)
|
||||
items.push(reservable_from_hash(item[:reservation], plan_info))
|
||||
elsif ['prepaid_pack', :prepaid_pack].include?(item.keys.first)
|
||||
|
@ -19,8 +19,8 @@ class PaymentGatewayService
|
||||
@gateway = service.new
|
||||
end
|
||||
|
||||
def create_subscription(payment_schedule, gateway_object_id)
|
||||
@gateway.create_subscription(payment_schedule, gateway_object_id)
|
||||
def create_subscription(payment_schedule, *args)
|
||||
@gateway.create_subscription(payment_schedule, *args)
|
||||
end
|
||||
|
||||
def extend_subscription(payment_schedule, gateway_object_id)
|
||||
|
@ -72,6 +72,7 @@ if member.subscription
|
||||
json.interval member.subscription.plan.interval
|
||||
json.interval_count member.subscription.plan.interval_count
|
||||
json.amount member.subscription.plan.amount ? (member.subscription.plan.amount / 100.0) : 0
|
||||
json.monthly_payment member.subscription.plan.monthly_payment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -6,7 +6,7 @@ module Payment; end
|
||||
# Abstract class that must be implemented by each payment gateway.
|
||||
# Provides methods to create remote objects on the payment gateway
|
||||
class Payment::Service
|
||||
def create_subscription(_payment_schedule, _gateway_object_id); end
|
||||
def create_subscription(_payment_schedule, *args); end
|
||||
|
||||
def create_user(_user_id); end
|
||||
|
||||
|
@ -14,7 +14,7 @@ class Stripe::Service < Payment::Service
|
||||
payment_schedule = price_details[:schedule][:payment_schedule]
|
||||
payment_schedule.payment_schedule_items = price_details[:schedule][:items]
|
||||
first_item = price_details[:schedule][:items].min_by(&:due_date)
|
||||
subscription = shopping_cart.items.find { |item| item.class == CartItem::Subscription }
|
||||
subscription = shopping_cart.items.find { |item| item.class == CartItem::Subscription }.to_object
|
||||
reservable_stp_id = shopping_cart.items.find { |item| item.is_a?(CartItem::Reservation) }&.to_object
|
||||
&.reservable&.payment_gateway_object&.gateway_object_id
|
||||
|
||||
@ -29,33 +29,46 @@ class Stripe::Service < Payment::Service
|
||||
|
||||
|
||||
stripe_key = Setting.get('stripe_secret_key')
|
||||
stp_subscription = Stripe::Subscription.create({
|
||||
customer: shopping_cart.customer.payment_gateway_object.gateway_object_id,
|
||||
cancel_at: (payment_schedule.payment_schedule_items.max_by(&:due_date).due_date + 1.month).to_i,
|
||||
add_invoice_items: items,
|
||||
coupon: payment_schedule.coupon&.code,
|
||||
items: [
|
||||
{ price: price[:id] }
|
||||
],
|
||||
default_payment_method: payment_method_id,
|
||||
expand: %w[latest_invoice.payment_intent]
|
||||
}, { api_key: stripe_key })
|
||||
return stp_subscription unless subscription.start_at
|
||||
|
||||
Stripe::SubscriptionSchedule.create({
|
||||
from_subscription: stp_subscription.id,
|
||||
start_date: subscription.start_at
|
||||
}, { api_key: stripe_key })
|
||||
|
||||
stp_subscription
|
||||
if subscription.start_at.nil?
|
||||
Stripe::Subscription.create({
|
||||
customer: shopping_cart.customer.payment_gateway_object.gateway_object_id,
|
||||
cancel_at: (payment_schedule.payment_schedule_items.max_by(&:due_date).due_date + 1.month).to_i,
|
||||
add_invoice_items: items,
|
||||
coupon: payment_schedule.coupon&.code,
|
||||
items: [
|
||||
{ price: price[:id] }
|
||||
],
|
||||
default_payment_method: payment_method_id,
|
||||
expand: %w[latest_invoice.payment_intent]
|
||||
}, { api_key: stripe_key })
|
||||
else
|
||||
Stripe::SubscriptionSchedule.create({
|
||||
customer: shopping_cart.customer.payment_gateway_object.gateway_object_id,
|
||||
start_date: subscription.start_at.nil? ? 'now' : subscription.start_at.to_i,
|
||||
end_behavior: 'cancel',
|
||||
phases: [
|
||||
{
|
||||
items: [
|
||||
{ price: price[:id] }
|
||||
],
|
||||
add_invoice_items: items,
|
||||
coupon: payment_schedule.coupon&.code,
|
||||
default_payment_method: payment_method_id,
|
||||
end_date: (
|
||||
payment_schedule.payment_schedule_items.max_by(&:due_date).due_date + 1.month
|
||||
).to_i
|
||||
}
|
||||
]
|
||||
}, { api_key: stripe_key })
|
||||
end
|
||||
end
|
||||
|
||||
def create_subscription(payment_schedule, stp_subscription_id)
|
||||
def create_subscription(payment_schedule, stp_object_id, stp_object_type)
|
||||
stripe_key = Setting.get('stripe_secret_key')
|
||||
|
||||
stp_subscription = Stripe::Subscription.retrieve({ id: stp_subscription_id }, { api_key: stripe_key })
|
||||
stp_subscription = Stripe::Item.new(stp_object_type, stp_object_id).retrieve
|
||||
|
||||
payment_method_id = stp_subscription.default_payment_method
|
||||
payment_method_id = Stripe::Customer.retrieve(stp_subscription.customer, api_key: stripe_key).invoice_settings.default_payment_method
|
||||
payment_method = Stripe::PaymentMethod.retrieve(payment_method_id, api_key: stripe_key)
|
||||
pgo = PaymentGatewayObject.new(item: payment_schedule)
|
||||
pgo.gateway_object = payment_method
|
||||
|
Loading…
x
Reference in New Issue
Block a user