1
0
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:
Sylvain 2021-10-15 17:31:01 +02:00
parent 3349cc3a2d
commit fd39eaf2f1
17 changed files with 83 additions and 44 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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