mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-19 13:54:25 +01:00
WIP: stripe update card
This commit is contained in:
parent
d1584604b3
commit
b0ef9e097d
@ -2,7 +2,7 @@
|
||||
|
||||
## Next release
|
||||
- Refactored theme builder to use scss files
|
||||
- Updated stripe gem to 5.21.0
|
||||
- Updated stripe gem to 5.29.0
|
||||
- Architecture documentation
|
||||
- Improved coupon creation/deletion workflow
|
||||
- Default texts for the login modal
|
||||
|
2
Gemfile
2
Gemfile
@ -90,7 +90,7 @@ gem 'sidekiq', '>= 6.0.7'
|
||||
gem 'sidekiq-cron'
|
||||
gem 'sidekiq-unique-jobs', '~> 6.0.22'
|
||||
|
||||
gem 'stripe', '5.21.0'
|
||||
gem 'stripe', '5.29.0'
|
||||
|
||||
gem 'recurrence'
|
||||
|
||||
|
@ -373,7 +373,7 @@ GEM
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
stripe (5.21.0)
|
||||
stripe (5.29.0)
|
||||
sync (0.5.0)
|
||||
sys-filesystem (1.3.3)
|
||||
ffi
|
||||
@ -488,7 +488,7 @@ DEPENDENCIES
|
||||
sidekiq-unique-jobs (~> 6.0.22)
|
||||
spring
|
||||
spring-watcher-listen (~> 2.0.0)
|
||||
stripe (= 5.21.0)
|
||||
stripe (= 5.29.0)
|
||||
sys-filesystem
|
||||
tzinfo-data
|
||||
vcr (= 3.0.1)
|
||||
|
@ -4,7 +4,7 @@
|
||||
class API::PaymentSchedulesController < API::ApiController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_payment_schedule, only: %i[download]
|
||||
before_action :set_payment_schedule_item, only: %i[cash_check refresh_item]
|
||||
before_action :set_payment_schedule_item, only: %i[cash_check refresh_item pay_item]
|
||||
|
||||
def list
|
||||
authorize PaymentSchedule
|
||||
@ -42,6 +42,20 @@ class API::PaymentSchedulesController < API::ApiController
|
||||
render json: { state: 'refreshed' }, status: :ok
|
||||
end
|
||||
|
||||
def pay_item
|
||||
authorize @payment_schedule_item.payment_schedule
|
||||
|
||||
stripe_key = Setting.get('stripe_secret_key')
|
||||
stp_invoice = Stripe::Invoice.pay(@payment_schedule_item.stp_invoice_id, {}, { api_key: stripe_key })
|
||||
|
||||
render json: { status: stp_invoice.status }, status: :ok
|
||||
rescue Stripe::StripeError => e
|
||||
stripe_key = Setting.get('stripe_secret_key')
|
||||
stp_invoice = Stripe::Invoice.retrieve(@payment_schedule_item.stp_invoice_id, api_key: stripe_key)
|
||||
|
||||
render json: { status: stp_invoice.status, error: e }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_payment_schedule
|
||||
|
@ -93,6 +93,17 @@ class API::PaymentsController < API::ApiController
|
||||
render json: e, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def update_card
|
||||
user = User.find(params[:user_id])
|
||||
key = Setting.get('stripe_secret_key')
|
||||
Stripe::Customer.update(user.stp_customer_id,
|
||||
{ invoice_settings: { default_payment_method: params[:payment_method_id] } },
|
||||
{ api_key: key })
|
||||
render json: { updated: true }, status: :ok
|
||||
rescue Stripe::StripeError => e
|
||||
render json: { updated: false, error: e }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def on_reservation_success(intent, details)
|
||||
|
@ -1,9 +1,9 @@
|
||||
import apiClient from './api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import {
|
||||
CashCheckResponse,
|
||||
CashCheckResponse, PayItemResponse,
|
||||
PaymentSchedule,
|
||||
PaymentScheduleIndexRequest,
|
||||
PaymentScheduleIndexRequest, RefreshItemResponse
|
||||
} from '../models/payment-schedule';
|
||||
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
|
||||
|
||||
@ -18,11 +18,16 @@ export default class PaymentScheduleAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
async refreshItem(paymentScheduleItemId: number): Promise<void> {
|
||||
async refreshItem(paymentScheduleItemId: number): Promise<RefreshItemResponse> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/refresh_item`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
async payItem(paymentScheduleItemId: number): Promise<PayItemResponse> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/pay_item`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static list (query: PaymentScheduleIndexRequest): IWrapPromise<Array<PaymentSchedule>> {
|
||||
const api = new PaymentScheduleAPI();
|
||||
return wrapPromise(api.list(query));
|
||||
|
@ -1,6 +1,6 @@
|
||||
import apiClient from './api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { CartItems, IntentConfirmation, PaymentConfirmation } from '../models/payment';
|
||||
import { CartItems, IntentConfirmation, PaymentConfirmation, UpdateCardResponse } from '../models/payment';
|
||||
|
||||
export default class PaymentAPI {
|
||||
static async confirm (stp_payment_method_id: string, cart_items: CartItems): Promise<PaymentConfirmation> {
|
||||
@ -16,6 +16,7 @@ export default class PaymentAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
// TODO, type the response
|
||||
static async confirmPaymentSchedule (setup_intent_id: string, cart_items: CartItems): Promise<any> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/payments/confirm_payment_schedule`, {
|
||||
setup_intent_id,
|
||||
@ -23,5 +24,13 @@ export default class PaymentAPI {
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async updateCard (user_id: number, stp_payment_method_id: string): Promise<UpdateCardResponse> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/payments/update_card`, {
|
||||
user_id,
|
||||
payment_method_id: stp_payment_method_id,
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,10 +9,12 @@ interface FabButtonProps {
|
||||
icon?: ReactNode,
|
||||
className?: string,
|
||||
disabled?: boolean,
|
||||
type?: 'submit' | 'reset' | 'button',
|
||||
form?: string,
|
||||
}
|
||||
|
||||
|
||||
export const FabButton: React.FC<FabButtonProps> = ({ onClick, icon, className, disabled, children }) => {
|
||||
export const FabButton: React.FC<FabButtonProps> = ({ onClick, icon, className, disabled, type, form, children }) => {
|
||||
/**
|
||||
* Check if the current component was provided an icon to display
|
||||
*/
|
||||
@ -30,10 +32,12 @@ export const FabButton: React.FC<FabButtonProps> = ({ onClick, icon, className,
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleClick} disabled={disabled} className={`fab-button ${className ? className : ''}`}>
|
||||
<button type={type} form={form} onClick={handleClick} disabled={disabled} className={`fab-button ${className ? className : ''}`}>
|
||||
{hasIcon() && <span className="fab-button--icon">{icon}</span>}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
FabButton.defaultProps = { type: 'button' };
|
||||
|
||||
|
@ -11,13 +11,18 @@ import PaymentScheduleAPI from '../api/payment-schedule';
|
||||
import { DocumentFilters } from './document-filters';
|
||||
import { PaymentSchedulesTable } from './payment-schedules-table';
|
||||
import { FabButton } from './fab-button';
|
||||
import { User } from '../models/user';
|
||||
|
||||
declare var Application: IApplication;
|
||||
|
||||
interface PaymentSchedulesListProps {
|
||||
currentUser: User
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
const paymentSchedulesList = PaymentScheduleAPI.list({ query: { page: 1, size: 20 } });
|
||||
|
||||
const PaymentSchedulesList: React.FC = () => {
|
||||
const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [paymentSchedules, setPaymentSchedules] = useState(paymentSchedulesList.read());
|
||||
@ -88,7 +93,7 @@ const PaymentSchedulesList: React.FC = () => {
|
||||
</div>
|
||||
{!hasSchedules() && <div>{t('app.admin.invoices.payment_schedules.no_payment_schedules')}</div>}
|
||||
{hasSchedules() && <div className="schedules-list">
|
||||
<PaymentSchedulesTable paymentSchedules={paymentSchedules} showCustomer={true} refreshList={handleRefreshList} />
|
||||
<PaymentSchedulesTable paymentSchedules={paymentSchedules} showCustomer={true} refreshList={handleRefreshList} operator={currentUser} />
|
||||
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.admin.invoices.payment_schedules.load_more')}</FabButton>}
|
||||
</div>}
|
||||
</div>
|
||||
@ -96,12 +101,12 @@ const PaymentSchedulesList: React.FC = () => {
|
||||
}
|
||||
|
||||
|
||||
const PaymentSchedulesListWrapper: React.FC = () => {
|
||||
const PaymentSchedulesListWrapper: React.FC<PaymentSchedulesListProps> = ({ currentUser }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PaymentSchedulesList />
|
||||
<PaymentSchedulesList currentUser={currentUser} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('paymentSchedulesList', react2angular(PaymentSchedulesListWrapper));
|
||||
Application.Components.component('paymentSchedulesList', react2angular(PaymentSchedulesListWrapper, ['currentUser']));
|
||||
|
@ -14,23 +14,33 @@ import { FabModal } from './fab-modal';
|
||||
import PaymentScheduleAPI from '../api/payment-schedule';
|
||||
import { StripeElements } from './stripe-elements';
|
||||
import { StripeConfirm } from './stripe-confirm';
|
||||
import stripeLogo from '../../../images/powered_by_stripe.png';
|
||||
import mastercardLogo from '../../../images/mastercard.png';
|
||||
import visaLogo from '../../../images/visa.png';
|
||||
import { StripeCardUpdate } from './stripe-card-update';
|
||||
import { User } from '../models/user';
|
||||
|
||||
declare var Fablab: IFablab;
|
||||
|
||||
interface PaymentSchedulesTableProps {
|
||||
paymentSchedules: Array<PaymentSchedule>,
|
||||
showCustomer?: boolean,
|
||||
refreshList: () => void
|
||||
refreshList: () => void,
|
||||
operator: User,
|
||||
}
|
||||
|
||||
const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList }) => {
|
||||
const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [showExpanded, setShowExpanded] = useState<Map<number, boolean>>(new Map());
|
||||
const [showConfirmCashing, setShowConfirmCashing] = useState<boolean>(false);
|
||||
const [showResolveAction, setShowResolveAction] = useState<boolean>(false);
|
||||
const [isConfirmActionDisabled, setConfirmActionDisabled] = useState<boolean>(true);
|
||||
const [showUpdateCard, setShowUpdateCard] = useState<boolean>(false);
|
||||
const [tempDeadline, setTempDeadline] = useState<PaymentScheduleItem>(null);
|
||||
const [tempSchedule, setTempSchedule] = useState<PaymentSchedule>(null);
|
||||
const [canSubmitUpdateCard, setCanSubmitUpdateCard] = useState<boolean>(true);
|
||||
const [errors, setErrors] = useState<string>(null);
|
||||
|
||||
/**
|
||||
* Check if the requested payment schedule is displayed with its deadlines (PaymentScheduleItem) or without them
|
||||
@ -123,7 +133,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
/**
|
||||
* Return the action button(s) for the given deadline
|
||||
*/
|
||||
const itemButtons = (item: PaymentScheduleItem): JSX.Element => {
|
||||
const itemButtons = (item: PaymentScheduleItem, schedule: PaymentSchedule): JSX.Element => {
|
||||
switch (item.state) {
|
||||
case PaymentScheduleItemState.Paid:
|
||||
return downloadButton(TargetType.Invoice, item.invoice_id);
|
||||
@ -143,7 +153,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
);
|
||||
case PaymentScheduleItemState.RequirePaymentMethod:
|
||||
return (
|
||||
<FabButton onClick={handleUpdateCard(item)}
|
||||
<FabButton onClick={handleUpdateCard(item, schedule)}
|
||||
icon={<i className="fas fa-credit-card" />}>
|
||||
{t('app.admin.invoices.schedules_table.update_card')}
|
||||
</FabButton>
|
||||
@ -222,18 +232,63 @@ 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
|
||||
*/
|
||||
const handleUpdateCard = (item: PaymentScheduleItem): ReactEventHandler => {
|
||||
const handleUpdateCard = (item: PaymentScheduleItem, paymentSchedule: PaymentSchedule): ReactEventHandler => {
|
||||
return (): void => {
|
||||
/*
|
||||
TODO
|
||||
- Notify the customer, collect new payment information, and create a new payment method
|
||||
- Attach the payment method to the customer
|
||||
- Update the default payment method
|
||||
- Pay the invoice using the new payment method
|
||||
*/
|
||||
setTempDeadline(item);
|
||||
setTempSchedule(paymentSchedule);
|
||||
toggleUpdateCardModal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog to update the bank card details
|
||||
*/
|
||||
const toggleUpdateCardModal = (): void => {
|
||||
setShowUpdateCard(!showUpdateCard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the logos, shown in the modal footer.
|
||||
*/
|
||||
const logoFooter = (): ReactNode => {
|
||||
return (
|
||||
<div className="stripe-modal-icons">
|
||||
<i className="fa fa-lock fa-2x m-r-sm pos-rlt" />
|
||||
<img src={stripeLogo} alt="powered by stripe" />
|
||||
<img src={mastercardLogo} alt="mastercard" />
|
||||
<img src={visaLogo} alt="visa" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the submit button is pushed, disable it to prevent double form submission
|
||||
*/
|
||||
const handleCardUpdateSubmit = (): void => {
|
||||
setCanSubmitUpdateCard(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the card was successfully updated, pay the invoice (using the new payment method) and close the modal
|
||||
*/
|
||||
const handleCardUpdateSuccess = (): void => {
|
||||
const api = new PaymentScheduleAPI();
|
||||
api.payItem(tempDeadline.id).then(() => {
|
||||
refreshList();
|
||||
toggleUpdateCardModal();
|
||||
}).catch((err) => {
|
||||
handleCardUpdateError(err.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When the card was not updated, show the error
|
||||
*/
|
||||
const handleCardUpdateError = (error): void => {
|
||||
setErrors(error);
|
||||
setCanSubmitUpdateCard(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className="schedules-table">
|
||||
@ -278,7 +333,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
<td>{formatDate(item.due_date)}</td>
|
||||
<td>{formatPrice(item.amount)}</td>
|
||||
<td>{formatState(item)}</td>
|
||||
<td>{itemButtons(item)}</td>
|
||||
<td>{itemButtons(item, p)}</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -314,6 +369,31 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
preventConfirm={isConfirmActionDisabled}>
|
||||
{tempDeadline && <StripeConfirm clientSecret={tempDeadline.client_secret} onResponse={toggleConfirmActionButton} />}
|
||||
</FabModal>
|
||||
<FabModal title={t('app.admin.invoices.schedules_table.update_card')}
|
||||
isOpen={showUpdateCard}
|
||||
toggleModal={toggleUpdateCardModal}
|
||||
closeButton={false}
|
||||
customFooter={logoFooter()}
|
||||
className="update-card-modal">
|
||||
{tempDeadline && tempSchedule && <StripeCardUpdate onSubmit={handleCardUpdateSubmit}
|
||||
onSuccess={handleCardUpdateSuccess}
|
||||
onError={handleCardUpdateError}
|
||||
customerId={tempSchedule.user.id}
|
||||
operator={operator}
|
||||
className="card-form" >
|
||||
{errors && <div className="stripe-errors">
|
||||
{errors}
|
||||
</div>}
|
||||
</StripeCardUpdate>}
|
||||
<div className="submit-card">
|
||||
{canSubmitUpdateCard && <button type="submit" disabled={!canSubmitUpdateCard} form="stripe-card" className="submit-card-btn">{t('app.admin.invoices.schedules_table.validate_button')}</button>}
|
||||
{!canSubmitUpdateCard && <div className="payment-pending">
|
||||
<div className="fa-2x">
|
||||
<i className="fas fa-circle-notch fa-spin" />
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</FabModal>
|
||||
</StripeElements>
|
||||
</div>
|
||||
</div>
|
||||
@ -322,10 +402,10 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
PaymentSchedulesTableComponent.defaultProps = { showCustomer: false };
|
||||
|
||||
|
||||
export const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList }) => {
|
||||
export const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PaymentSchedulesTableComponent paymentSchedules={paymentSchedules} showCustomer={showCustomer} refreshList={refreshList} />
|
||||
<PaymentSchedulesTableComponent paymentSchedules={paymentSchedules} showCustomer={showCustomer} refreshList={refreshList} operator={operator} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
101
app/frontend/src/javascript/components/stripe-card-update.tsx
Normal file
101
app/frontend/src/javascript/components/stripe-card-update.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import React, { FormEvent } from 'react';
|
||||
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
||||
import { SetupIntent } from "@stripe/stripe-js";
|
||||
import PaymentAPI from '../api/payment';
|
||||
import { PaymentConfirmation } from '../models/payment';
|
||||
import { User } from '../models/user';
|
||||
|
||||
interface StripeCardUpdateProps {
|
||||
onSubmit: () => void,
|
||||
onSuccess: (result: SetupIntent|PaymentConfirmation|any) => void,
|
||||
onError: (message: string) => void,
|
||||
customerId: number,
|
||||
operator: User,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple form component to collect and update the credit card details, for Stripe.
|
||||
*
|
||||
* The form validation button must be created elsewhere, using the attribute form="stripe-card".
|
||||
*/
|
||||
export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, onSuccess, onError, className, customerId, operator, children }) => {
|
||||
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
|
||||
/**
|
||||
* Handle the submission of the form. Depending on the configuration, it will create the payment method on Stripe,
|
||||
* or it will process a payment with the inputted card.
|
||||
*/
|
||||
const handleSubmit = async (event: FormEvent): Promise<void> => {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
|
||||
// Stripe.js has not loaded yet
|
||||
if (!stripe || !elements) { return; }
|
||||
|
||||
const cardElement = elements.getElement(CardElement);
|
||||
const { error, paymentMethod } = await stripe.createPaymentMethod({
|
||||
type: 'card',
|
||||
card: cardElement,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
// stripe error
|
||||
onError(error.message);
|
||||
} else {
|
||||
try {
|
||||
// we start by associating the payment method with the user
|
||||
const { client_secret } = await PaymentAPI.setupIntent(customerId);
|
||||
const { error } = await stripe.confirmCardSetup(client_secret, {
|
||||
payment_method: paymentMethod.id,
|
||||
mandate_data: {
|
||||
customer_acceptance: {
|
||||
type: 'online',
|
||||
online: {
|
||||
ip_address: operator.ip_address,
|
||||
user_agent: navigator.userAgent
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if (error) {
|
||||
onError(error.message);
|
||||
} else {
|
||||
// then we update the default payment method
|
||||
const res = await PaymentAPI.updateCard(customerId, paymentMethod.id);
|
||||
onSuccess(res);
|
||||
}
|
||||
} catch (err) {
|
||||
// catch api errors
|
||||
onError(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the Stripe's card input
|
||||
*/
|
||||
const cardOptions = {
|
||||
style: {
|
||||
base: {
|
||||
fontSize: '16px',
|
||||
color: '#424770',
|
||||
'::placeholder': { color: '#aab7c4' }
|
||||
},
|
||||
invalid: {
|
||||
color: '#9e2146',
|
||||
iconColor: '#9e2146'
|
||||
},
|
||||
},
|
||||
hidePostalCode: true
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} id="stripe-card" className={className}>
|
||||
<CardElement options={cardOptions} />
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { StripeIbanElement } from '@stripe/stripe-js';
|
||||
|
||||
export enum PaymentScheduleItemState {
|
||||
New = 'new',
|
||||
Pending = 'pending',
|
||||
@ -42,6 +44,7 @@ export interface PaymentSchedule {
|
||||
created_at: Date,
|
||||
chained_footprint: boolean,
|
||||
user: {
|
||||
id: number,
|
||||
name: string
|
||||
},
|
||||
operator: {
|
||||
@ -65,3 +68,12 @@ export interface CashCheckResponse {
|
||||
state: PaymentScheduleItemState,
|
||||
payment_method: PaymentMethod
|
||||
}
|
||||
|
||||
export interface RefreshItemResponse {
|
||||
state: 'refreshed'
|
||||
}
|
||||
|
||||
export interface PayItemResponse {
|
||||
status: 'draft' | 'open' | 'paid' | 'uncollectible' | 'void',
|
||||
error?: string
|
||||
}
|
||||
|
@ -24,3 +24,8 @@ export interface CartItems {
|
||||
subscription?: SubscriptionRequest,
|
||||
coupon_code?: string
|
||||
}
|
||||
|
||||
export interface UpdateCardResponse {
|
||||
updated: boolean,
|
||||
error?: string
|
||||
}
|
||||
|
@ -121,3 +121,60 @@
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.fab-modal.update-card-modal {
|
||||
.fab-modal-content {
|
||||
.card-form {
|
||||
background-color: #f4f3f3;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px 6px 0 0;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
padding: 15px;
|
||||
|
||||
.stripe-errors {
|
||||
padding: 4px 0;
|
||||
color: #9e2146;
|
||||
}
|
||||
}
|
||||
.submit-card {
|
||||
.submit-card-btn {
|
||||
width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 0 6px 6px;
|
||||
border-top: 0;
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
background-color: #1d98ec;
|
||||
margin-bottom: 15px;
|
||||
|
||||
&[disabled] {
|
||||
background-color: lighten(#1d98ec, 20%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.payment-pending {
|
||||
@extend .submit-card-btn;
|
||||
@extend .submit-card-btn[disabled];
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fab-modal-footer {
|
||||
.stripe-modal-icons {
|
||||
& {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fa.fa-lock {
|
||||
top: 7px;
|
||||
color: #9edd78;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab heading="{{ 'app.admin.invoices.payment_schedules_list' | translate }}" ng-show="$root.modules.invoicing" index="4">
|
||||
<payment-schedules-list />
|
||||
<payment-schedules-list current-user="currentUser" />
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab heading="{{ 'app.admin.invoices.invoicing_settings' | translate }}" index="1" class="invoices-settings">
|
||||
|
@ -2,19 +2,15 @@
|
||||
|
||||
# Check the access policies for API::PaymentSchedulesController
|
||||
class PaymentSchedulePolicy < ApplicationPolicy
|
||||
def list?
|
||||
user.admin? || user.manager?
|
||||
%w[list? cash_check?].each do |action|
|
||||
define_method action do
|
||||
user.admin? || user.manager?
|
||||
end
|
||||
end
|
||||
|
||||
def cash_check?
|
||||
user.admin? || user.manager?
|
||||
end
|
||||
|
||||
def refresh_item?
|
||||
user.admin? || user.manager? || (record.invoicing_profile.user_id == user.id)
|
||||
end
|
||||
|
||||
def download?
|
||||
user.admin? || user.manager? || (record.invoicing_profile.user_id == user.id)
|
||||
%w[refresh_item? download? pay_item?].each do |action|
|
||||
define_method action do
|
||||
user.admin? || user.manager? || (record.invoicing_profile.user_id == user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -4,6 +4,7 @@ json.extract! payment_schedule, :id, :reference, :created_at, :payment_method
|
||||
json.total payment_schedule.total / 100.00
|
||||
json.chained_footprint payment_schedule.check_footprint
|
||||
json.user do
|
||||
json.id payment_schedule.invoicing_profile&.user&.id
|
||||
json.name payment_schedule.invoicing_profile.full_name
|
||||
end
|
||||
if payment_schedule.operator_profile
|
||||
|
@ -669,6 +669,7 @@ en:
|
||||
confirm_button: "Confirm"
|
||||
resolve_action: "Resolve the action"
|
||||
ok_button: "OK"
|
||||
validate_button: "Validate the new card"
|
||||
document_filters:
|
||||
reference: "Reference"
|
||||
customer: "Customer"
|
||||
|
@ -669,6 +669,7 @@ fr:
|
||||
confirm_button: "Confirmer"
|
||||
resolve_action: "Résoudre l'action"
|
||||
ok_button: "OK"
|
||||
validate_button: "Valider la nouvelle carte"
|
||||
document_filters:
|
||||
reference: "Référence"
|
||||
customer: "Client"
|
||||
|
@ -116,6 +116,7 @@ Rails.application.routes.draw do
|
||||
get 'download', on: :member
|
||||
post 'items/:id/cash_check', action: 'cash_check', on: :collection
|
||||
post 'items/:id/refresh_item', action: 'refresh_item', on: :collection
|
||||
post 'items/:id/pay_item', action: 'pay_item', on: :collection
|
||||
end
|
||||
|
||||
resources :i_calendar, only: %i[index create destroy] do
|
||||
@ -174,6 +175,7 @@ Rails.application.routes.draw do
|
||||
get 'payments/online_payment_status' => 'payments/online_payment_status'
|
||||
get 'payments/setup_intent/:user_id' => 'payments#setup_intent'
|
||||
post 'payments/confirm_payment_schedule' => 'payments#confirm_payment_schedule'
|
||||
post 'payments/update_card' => 'payments#update_card'
|
||||
|
||||
# FabAnalytics
|
||||
get 'analytics/data' => 'analytics#data'
|
||||
|
Loading…
x
Reference in New Issue
Block a user