mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-06 01:08:21 +01:00
Merge branch 'monthly-payment' into staging
This commit is contained in:
commit
c3636e57a6
@ -10,6 +10,7 @@
|
|||||||
- Fix a bug: warning message overflow in credit wallet modal
|
- Fix a bug: warning message overflow in credit wallet modal
|
||||||
- Fix a bug: when using a cash coupon, the amount shown in the statistics is invalid
|
- Fix a bug: when using a cash coupon, the amount shown in the statistics is invalid
|
||||||
- Fix a bug: unable to create a coupon on stripe
|
- Fix a bug: unable to create a coupon on stripe
|
||||||
|
- Fix a bug: no notifications for refunds generated on wallet credit
|
||||||
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`
|
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`
|
||||||
- [TODO DEPLOY] `rails fablab:stripe:set_product_id`
|
- [TODO DEPLOY] `rails fablab:stripe:set_product_id`
|
||||||
- [TODO DEPLOY] `rails fablab:setup:add_schedule_reference`
|
- [TODO DEPLOY] `rails fablab:setup:add_schedule_reference`
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
class API::PaymentSchedulesController < API::ApiController
|
class API::PaymentSchedulesController < API::ApiController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_payment_schedule, only: %i[download]
|
before_action :set_payment_schedule, only: %i[download]
|
||||||
before_action :set_payment_schedule_item, only: %i[cash_check]
|
before_action :set_payment_schedule_item, only: %i[cash_check refresh_item]
|
||||||
|
|
||||||
def list
|
def list
|
||||||
authorize PaymentSchedule
|
authorize PaymentSchedule
|
||||||
@ -27,12 +27,19 @@ class API::PaymentSchedulesController < API::ApiController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def cash_check
|
def cash_check
|
||||||
schedule = @payment_schedule_item.payment_schedule
|
authorize @payment_schedule_item.payment_schedule
|
||||||
authorize schedule
|
|
||||||
PaymentScheduleService.new.generate_invoice(@payment_schedule_item)
|
PaymentScheduleService.new.generate_invoice(@payment_schedule_item)
|
||||||
@payment_schedule_item.update_attributes(state: 'paid', payment_method: 'check')
|
attrs = { state: 'paid', payment_method: 'check' }
|
||||||
|
@payment_schedule_item.update_attributes(attrs)
|
||||||
|
|
||||||
render :show, status: :ok, location: schedule
|
render json: attrs, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_item
|
||||||
|
authorize @payment_schedule_item.payment_schedule
|
||||||
|
PaymentScheduleItemWorker.new.perform(params[:id])
|
||||||
|
|
||||||
|
render json: { state: 'refreshed' }, status: :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import apiClient from './api-client';
|
import apiClient from './api-client';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { PaymentSchedule, PaymentScheduleIndexRequest, PaymentScheduleItem } from '../models/payment-schedule';
|
import {
|
||||||
|
CashCheckResponse,
|
||||||
|
PaymentSchedule,
|
||||||
|
PaymentScheduleIndexRequest,
|
||||||
|
} from '../models/payment-schedule';
|
||||||
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
|
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
|
||||||
|
|
||||||
export default class PaymentScheduleAPI {
|
export default class PaymentScheduleAPI {
|
||||||
@ -9,11 +13,16 @@ export default class PaymentScheduleAPI {
|
|||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async cashCheck(paymentScheduleItemId: number) {
|
async cashCheck(paymentScheduleItemId: number): Promise<CashCheckResponse> {
|
||||||
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/cash_check`);
|
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/cash_check`);
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshItem(paymentScheduleItemId: number): Promise<void> {
|
||||||
|
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/refresh_item`);
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
static list (query: PaymentScheduleIndexRequest): IWrapPromise<Array<PaymentSchedule>> {
|
static list (query: PaymentScheduleIndexRequest): IWrapPromise<Array<PaymentSchedule>> {
|
||||||
const api = new PaymentScheduleAPI();
|
const api = new PaymentScheduleAPI();
|
||||||
return wrapPromise(api.list(query));
|
return wrapPromise(api.list(query));
|
||||||
|
@ -8,10 +8,11 @@ interface FabButtonProps {
|
|||||||
onClick?: (event: SyntheticEvent) => void,
|
onClick?: (event: SyntheticEvent) => void,
|
||||||
icon?: ReactNode,
|
icon?: ReactNode,
|
||||||
className?: string,
|
className?: string,
|
||||||
|
disabled?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const FabButton: React.FC<FabButtonProps> = ({ onClick, icon, className, children }) => {
|
export const FabButton: React.FC<FabButtonProps> = ({ onClick, icon, className, disabled, children }) => {
|
||||||
/**
|
/**
|
||||||
* Check if the current component was provided an icon to display
|
* Check if the current component was provided an icon to display
|
||||||
*/
|
*/
|
||||||
@ -29,7 +30,7 @@ export const FabButton: React.FC<FabButtonProps> = ({ onClick, icon, className,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button onClick={handleClick} className={`fab-button ${className ? className : ''}`}>
|
<button onClick={handleClick} disabled={disabled} className={`fab-button ${className ? className : ''}`}>
|
||||||
{hasIcon() && <span className="fab-button--icon">{icon}</span>}
|
{hasIcon() && <span className="fab-button--icon">{icon}</span>}
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
@ -27,12 +27,13 @@ interface FabModalProps {
|
|||||||
className?: string,
|
className?: string,
|
||||||
width?: ModalSize,
|
width?: ModalSize,
|
||||||
customFooter?: ReactNode,
|
customFooter?: ReactNode,
|
||||||
onConfirm?: (event: SyntheticEvent) => void
|
onConfirm?: (event: SyntheticEvent) => void,
|
||||||
|
preventConfirm?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const blackLogoFile = CustomAssetAPI.get(CustomAssetName.LogoBlackFile);
|
const blackLogoFile = CustomAssetAPI.get(CustomAssetName.LogoBlackFile);
|
||||||
|
|
||||||
export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal, children, confirmButton, className, width = 'sm', closeButton, customFooter, onConfirm }) => {
|
export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal, children, confirmButton, className, width = 'sm', closeButton, customFooter, onConfirm, preventConfirm }) => {
|
||||||
const { t } = useTranslation('shared');
|
const { t } = useTranslation('shared');
|
||||||
const blackLogo = blackLogoFile.read();
|
const blackLogo = blackLogoFile.read();
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen}onConfirm
|
<Modal isOpen={isOpen}
|
||||||
className={`fab-modal fab-modal-${width} ${className}`}
|
className={`fab-modal fab-modal-${width} ${className}`}
|
||||||
overlayClassName="fab-modal-overlay"
|
overlayClassName="fab-modal-overlay"
|
||||||
onRequestClose={toggleModal}>
|
onRequestClose={toggleModal}>
|
||||||
@ -76,7 +77,7 @@ export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal,
|
|||||||
<div className="fab-modal-footer">
|
<div className="fab-modal-footer">
|
||||||
<Loader>
|
<Loader>
|
||||||
{hasCloseButton() &&<FabButton className="modal-btn--close" onClick={toggleModal}>{t('app.shared.buttons.close')}</FabButton>}
|
{hasCloseButton() &&<FabButton className="modal-btn--close" onClick={toggleModal}>{t('app.shared.buttons.close')}</FabButton>}
|
||||||
{hasConfirmButton() && <FabButton className="modal-btn--confirm" onClick={onConfirm}>{confirmButton}</FabButton>}
|
{hasConfirmButton() && <FabButton className="modal-btn--confirm" disabled={preventConfirm} onClick={onConfirm}>{confirmButton}</FabButton>}
|
||||||
{hasCustomFooter() && customFooter}
|
{hasCustomFooter() && customFooter}
|
||||||
</Loader>
|
</Loader>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +12,8 @@ import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '
|
|||||||
import { FabButton } from './fab-button';
|
import { FabButton } from './fab-button';
|
||||||
import { FabModal } from './fab-modal';
|
import { FabModal } from './fab-modal';
|
||||||
import PaymentScheduleAPI from '../api/payment-schedule';
|
import PaymentScheduleAPI from '../api/payment-schedule';
|
||||||
|
import { StripeElements } from './stripe-elements';
|
||||||
|
import { StripeConfirm } from './stripe-confirm';
|
||||||
|
|
||||||
declare var Fablab: IFablab;
|
declare var Fablab: IFablab;
|
||||||
|
|
||||||
@ -26,6 +28,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
|||||||
|
|
||||||
const [showExpanded, setShowExpanded] = useState<Map<number, boolean>>(new Map());
|
const [showExpanded, setShowExpanded] = useState<Map<number, boolean>>(new Map());
|
||||||
const [showConfirmCashing, setShowConfirmCashing] = useState<boolean>(false);
|
const [showConfirmCashing, setShowConfirmCashing] = useState<boolean>(false);
|
||||||
|
const [showResolveAction, setShowResolveAction] = useState<boolean>(false);
|
||||||
|
const [isConfirmActionDisabled, setConfirmActionDisabled] = useState<boolean>(true);
|
||||||
const [tempDeadline, setTempDeadline] = useState<PaymentScheduleItem>(null);
|
const [tempDeadline, setTempDeadline] = useState<PaymentScheduleItem>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -149,6 +153,9 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the user's clicks on the "cash check" button: show a confirmation modal
|
||||||
|
*/
|
||||||
const handleConfirmCheckPayment = (item: PaymentScheduleItem): ReactEventHandler => {
|
const handleConfirmCheckPayment = (item: PaymentScheduleItem): ReactEventHandler => {
|
||||||
return (): void => {
|
return (): void => {
|
||||||
setTempDeadline(item);
|
setTempDeadline(item);
|
||||||
@ -156,11 +163,16 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After the user has confirmed that he wants to cash the check, update the API, refresh the list and close the modal.
|
||||||
|
*/
|
||||||
const onCheckCashingConfirmed = (): void => {
|
const onCheckCashingConfirmed = (): void => {
|
||||||
const api = new PaymentScheduleAPI();
|
const api = new PaymentScheduleAPI();
|
||||||
api.cashCheck(tempDeadline.id).then(() => {
|
api.cashCheck(tempDeadline.id).then((res) => {
|
||||||
refreshList();
|
if (res.state === PaymentScheduleItemState.Paid) {
|
||||||
toggleConfirmCashingModal();
|
refreshList();
|
||||||
|
toggleConfirmCashingModal();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,37 +184,44 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically build the content of the modal depending on the currently selected deadline
|
* Show/hide the modal dialog that trigger the card "action".
|
||||||
*/
|
*/
|
||||||
const cashingModalContent = (): ReactNode => {
|
const toggleResolveActionModal = (): void => {
|
||||||
if (tempDeadline) {
|
setShowResolveAction(!showResolveAction);
|
||||||
return (
|
|
||||||
<span>{t('app.admin.invoices.schedules_table.confirm_check_cashing_body', {
|
|
||||||
AMOUNT: formatPrice(tempDeadline.amount),
|
|
||||||
DATE: formatDate(tempDeadline.due_date)
|
|
||||||
})}</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the user's clicks on the "resolve" button: show a modal that will trigger the action
|
||||||
|
*/
|
||||||
const handleSolveAction = (item: PaymentScheduleItem): ReactEventHandler => {
|
const handleSolveAction = (item: PaymentScheduleItem): ReactEventHandler => {
|
||||||
return (): void => {
|
return (): void => {
|
||||||
/*
|
setTempDeadline(item);
|
||||||
TODO
|
toggleResolveActionModal();
|
||||||
- create component wrapped with <StripeElements>
|
|
||||||
- stripe.confirmCardSetup(item.client_secret).then(function(result) {
|
|
||||||
if (result.error) {
|
|
||||||
// Display error.message in your UI.
|
|
||||||
} else {
|
|
||||||
// The setup has succeeded. Display a success message.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After the action was done (successfully or not), ask the API to refresh the item status, then refresh the list and close the modal
|
||||||
|
*/
|
||||||
|
const afterAction = (): void => {
|
||||||
|
toggleConfirmActionButton();
|
||||||
|
const api = new PaymentScheduleAPI();
|
||||||
|
api.refreshItem(tempDeadline.id).then(() => {
|
||||||
|
refreshList();
|
||||||
|
toggleResolveActionModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable/disable the confirm button of the "action" modal
|
||||||
|
*/
|
||||||
|
const toggleConfirmActionButton = (): void => {
|
||||||
|
setConfirmActionDisabled(!isConfirmActionDisabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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): ReactEventHandler => {
|
||||||
return (): void => {
|
return (): void => {
|
||||||
/*
|
/*
|
||||||
@ -279,8 +298,23 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
|||||||
onConfirm={onCheckCashingConfirmed}
|
onConfirm={onCheckCashingConfirmed}
|
||||||
closeButton={true}
|
closeButton={true}
|
||||||
confirmButton={t('app.admin.invoices.schedules_table.confirm_button')}>
|
confirmButton={t('app.admin.invoices.schedules_table.confirm_button')}>
|
||||||
{cashingModalContent()}
|
{tempDeadline && <span>
|
||||||
|
{t('app.admin.invoices.schedules_table.confirm_check_cashing_body', {
|
||||||
|
AMOUNT: formatPrice(tempDeadline.amount),
|
||||||
|
DATE: formatDate(tempDeadline.due_date)
|
||||||
|
})}
|
||||||
|
</span>}
|
||||||
</FabModal>
|
</FabModal>
|
||||||
|
<StripeElements>
|
||||||
|
<FabModal title={t('app.admin.invoices.schedules_table.resolve_action')}
|
||||||
|
isOpen={showResolveAction}
|
||||||
|
toggleModal={toggleResolveActionModal}
|
||||||
|
onConfirm={afterAction}
|
||||||
|
confirmButton={t('app.admin.invoices.schedules_table.ok_button')}
|
||||||
|
preventConfirm={isConfirmActionDisabled}>
|
||||||
|
{tempDeadline && <StripeConfirm clientSecret={tempDeadline.client_secret} onResponse={toggleConfirmActionButton} />}
|
||||||
|
</FabModal>
|
||||||
|
</StripeElements>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
34
app/frontend/src/javascript/components/stripe-confirm.tsx
Normal file
34
app/frontend/src/javascript/components/stripe-confirm.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useStripe } from '@stripe/react-stripe-js';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface StripeConfirmProps {
|
||||||
|
clientSecret: string,
|
||||||
|
onResponse: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StripeConfirm: React.FC<StripeConfirmProps> = ({ clientSecret, onResponse }) => {
|
||||||
|
const stripe = useStripe();
|
||||||
|
const { t } = useTranslation('shared');
|
||||||
|
|
||||||
|
const [message, setMessage] = useState<string>(t('app.shared.stripe_confirm.pending'));
|
||||||
|
const [type, setType] = useState<string>('info');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
stripe.confirmCardPayment(clientSecret).then(function(result) {
|
||||||
|
onResponse();
|
||||||
|
if (result.error) {
|
||||||
|
// Display error.message in your UI.
|
||||||
|
setType('error');
|
||||||
|
setMessage(result.error.message);
|
||||||
|
} else {
|
||||||
|
// The setup has succeeded. Display a success message.
|
||||||
|
setType('success');
|
||||||
|
setMessage(t('app.shared.stripe_confirm.success'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [])
|
||||||
|
return <div className="stripe-confirm">
|
||||||
|
<div className={`message--${type}`}><span className="message-text">{message}</span></div>
|
||||||
|
</div>;
|
||||||
|
}
|
@ -21,8 +21,10 @@ export interface PaymentScheduleItem {
|
|||||||
client_secret?: string,
|
client_secret?: string,
|
||||||
details: {
|
details: {
|
||||||
recurring: number,
|
recurring: number,
|
||||||
adjustment: number,
|
adjustment?: number,
|
||||||
other_items: number
|
other_items?: number,
|
||||||
|
without_coupon?: number,
|
||||||
|
subscription_id: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,3 +60,8 @@ export interface PaymentScheduleIndexRequest {
|
|||||||
size: number
|
size: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CashCheckResponse {
|
||||||
|
state: PaymentScheduleItemState,
|
||||||
|
payment_method: PaymentMethod
|
||||||
|
}
|
||||||
|
@ -30,5 +30,6 @@
|
|||||||
@import "modules/document-filters";
|
@import "modules/document-filters";
|
||||||
@import "modules/payment-schedules-table";
|
@import "modules/payment-schedules-table";
|
||||||
@import "modules/payment-schedules-list";
|
@import "modules/payment-schedules-list";
|
||||||
|
@import "modules/stripe-confirm";
|
||||||
|
|
||||||
@import "app.responsive";
|
@import "app.responsive";
|
||||||
|
@ -33,6 +33,15 @@
|
|||||||
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
|
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
color: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled]:hover {
|
||||||
|
color: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
&--icon {
|
&--icon {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
41
app/frontend/src/stylesheets/modules/stripe-confirm.scss
Normal file
41
app/frontend/src/stylesheets/modules/stripe-confirm.scss
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
@keyframes spin { 100% { transform:rotate(360deg); } }
|
||||||
|
|
||||||
|
.stripe-confirm {
|
||||||
|
.message {
|
||||||
|
&--success:before {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-weight: 900;
|
||||||
|
content: "\f00c";
|
||||||
|
color: #3c763d;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error:before {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-weight: 900;
|
||||||
|
content: "\f00d";
|
||||||
|
color: #840b0f;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--info:before {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-weight: 900;
|
||||||
|
content: "\f1ce";
|
||||||
|
color: #a0a0a0;
|
||||||
|
margin-right: 2em;
|
||||||
|
animation:spin 2s linear infinite;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--info {
|
||||||
|
.message-text {
|
||||||
|
margin-left: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
@ -40,7 +40,7 @@ class Coupon < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def usages
|
def usages
|
||||||
invoices.count + payment_schedule.count
|
invoices.count
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -19,6 +19,6 @@ class PaymentScheduleItem < Footprintable
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.columns_out_of_footprint
|
def self.columns_out_of_footprint
|
||||||
%w[invoice_id stp_invoice_id state payment_method]
|
%w[invoice_id stp_invoice_id state payment_method client_secret]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -10,6 +10,10 @@ class PaymentSchedulePolicy < ApplicationPolicy
|
|||||||
user.admin? || user.manager?
|
user.admin? || user.manager?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def refresh_item?
|
||||||
|
user.admin? || user.manager? || (record.invoicing_profile.user_id == user.id)
|
||||||
|
end
|
||||||
|
|
||||||
def download?
|
def download?
|
||||||
user.admin? || user.manager? || (record.invoicing_profile.user_id == user.id)
|
user.admin? || user.manager? || (record.invoicing_profile.user_id == user.id)
|
||||||
end
|
end
|
||||||
|
@ -25,7 +25,7 @@ class CouponService
|
|||||||
unless coupon_object.nil?
|
unless coupon_object.nil?
|
||||||
if coupon_object.status(user_id, total) == 'active'
|
if coupon_object.status(user_id, total) == 'active'
|
||||||
if coupon_object.type == 'percent_off'
|
if coupon_object.type == 'percent_off'
|
||||||
price -= price * coupon_object.percent_off / 100.00
|
price -= (price * coupon_object.percent_off / 100.00).truncate
|
||||||
elsif coupon_object.type == 'amount_off'
|
elsif coupon_object.type == 'amount_off'
|
||||||
# do not apply cash coupon unless it has a lower amount that the total price
|
# do not apply cash coupon unless it has a lower amount that the total price
|
||||||
price -= coupon_object.amount_off if coupon_object.amount_off <= price
|
price -= coupon_object.amount_off if coupon_object.amount_off <= price
|
||||||
|
@ -142,7 +142,7 @@ class PaymentScheduleService
|
|||||||
|
|
||||||
##
|
##
|
||||||
# The first PaymentScheduleItem contains references to the reservation price (if any) and to the adjustement price
|
# The first PaymentScheduleItem contains references to the reservation price (if any) and to the adjustement price
|
||||||
# for the subscription (if any)
|
# for the subscription (if any) and the wallet transaction (if any)
|
||||||
##
|
##
|
||||||
def complete_first_invoice(payment_schedule_item, invoice)
|
def complete_first_invoice(payment_schedule_item, invoice)
|
||||||
# sub-prices for the subscription and the reservation
|
# sub-prices for the subscription and the reservation
|
||||||
@ -157,6 +157,10 @@ class PaymentScheduleService
|
|||||||
reservation = payment_schedule_item.payment_schedule.scheduled
|
reservation = payment_schedule_item.payment_schedule.scheduled
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# the wallet transaction
|
||||||
|
invoice[:wallet_amount] = payment_schedule_item.payment_schedule.wallet_amount
|
||||||
|
invoice[:wallet_transaction_id] = payment_schedule_item.payment_schedule.wallet_transaction_id
|
||||||
|
|
||||||
# build the invoice items
|
# build the invoice items
|
||||||
generate_invoice_items(invoice, details, subscription: subscription, reservation: reservation)
|
generate_invoice_items(invoice, details, subscription: subscription, reservation: reservation)
|
||||||
end
|
end
|
||||||
|
@ -4,6 +4,7 @@ $primary-dark: <%= Stylesheet.primary_dark %> !default;
|
|||||||
|
|
||||||
$secondary: <%= Stylesheet.secondary %> !default;
|
$secondary: <%= Stylesheet.secondary %> !default;
|
||||||
$secondary-light: <%= Stylesheet.secondary_light %> !default;
|
$secondary-light: <%= Stylesheet.secondary_light %> !default;
|
||||||
|
$secondary-lighter: lighten(<%= Stylesheet.secondary_light %>, 20%) !default;
|
||||||
$secondary-dark: <%= Stylesheet.secondary_dark %> !default;
|
$secondary-dark: <%= Stylesheet.secondary_dark %> !default;
|
||||||
|
|
||||||
$primary-text-color: <%= Stylesheet.primary_text_color %> !default;
|
$primary-text-color: <%= Stylesheet.primary_text_color %> !default;
|
||||||
@ -320,10 +321,22 @@ section#cookies-modal div.cookies-consent .cookies-actions button.accept {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $secondary-dark !important;
|
background-color: $secondary-dark;
|
||||||
border-color: $secondary-dark !important;
|
border-color: $secondary-dark;
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
background-color: $secondary-light;
|
||||||
|
color: $secondary-lighter;
|
||||||
|
border-color: $secondary-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled]:hover {
|
||||||
|
background-color: $secondary-light;
|
||||||
|
color: $secondary-lighter;
|
||||||
|
border-color: $secondary-lighter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
<p>
|
<p>
|
||||||
<%= t('.body.remember',
|
<%= t('.body.remember',
|
||||||
REFERENCE: @attached_object.payment_schedule.reference,
|
REFERENCE: @attached_object.payment_schedule.reference,
|
||||||
AMOUNT: number_to_currency(@attached_object.amount),
|
AMOUNT: number_to_currency(@attached_object.amount / 100.00),
|
||||||
DATE: I18n.l @attached_object.due_date, format: :long) %>
|
DATE: I18n.l(@attached_object.due_date, format: :long)) %>
|
||||||
<%= t('.body.date') %>
|
<%= t('.body.date') %>
|
||||||
</p>
|
</p>
|
||||||
<p><%= t('.body.confirm') %></p>
|
<p><%= t('.body.confirm') %></p>
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
<p>
|
<p>
|
||||||
<%= t('.body.remember',
|
<%= t('.body.remember',
|
||||||
REFERENCE: @attached_object.payment_schedule.reference,
|
REFERENCE: @attached_object.payment_schedule.reference,
|
||||||
AMOUNT: number_to_currency(@attached_object.amount),
|
AMOUNT: number_to_currency(@attached_object.amount / 100.00),
|
||||||
DATE: I18n.l @attached_object.due_date, format: :long) %>
|
DATE: I18n.l(@attached_object.due_date, format: :long)) %>
|
||||||
<%= t('.body.error') %>
|
<%= t('.body.error') %>
|
||||||
</p>
|
</p>
|
||||||
<p><%= t('.body.action') %></p>
|
<p><%= t('.body.action') %></p>
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
<p><%= t('.body.refund_created',
|
<p><%= t('.body.refund_created',
|
||||||
AMOUNT: number_to_currency(@attached_object.total / 100.00),
|
AMOUNT: number_to_currency(@attached_object.total / 100.00),
|
||||||
INVOICE: @attached_object.invoice.reference,
|
INVOICE: @attached_object.invoice.reference,
|
||||||
USER: @attached_object.invoicing_profile&.full_name) %>
|
USER: @attached_object.invoicing_profile&.full_name) if @attached_object.invoice %>
|
||||||
|
<%= t('.body.wallet_refund_created',
|
||||||
|
AMOUNT: number_to_currency(@attached_object.total / 100.00),
|
||||||
|
USER: @attached_object.invoicing_profile&.full_name) if @attached_object.invoiced_type === WalletTransaction.name %>
|
||||||
</p>
|
</p>
|
||||||
<p><a href="<%= "#{root_url}api/invoices/#{@attached_object.id}/download" %>" target="_blank"><%= t('.body.download') %></a></p>
|
<p><a href="<%= "#{root_url}api/invoices/#{@attached_object.id}/download" %>" target="_blank"><%= t('.body.download') %></a></p>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<p><%= t('.body.member_cancelled', NAME: @attached_object.reservation.user&.profile&.full_name || t('api.notifications.deleted_user')) %></p>
|
<p><%= t('.body.member_cancelled', NAME: @attached_object.reservation.user&.profile&.full_name || t('api.notifications.deleted_user')) %></p>
|
||||||
<p><%= t('.body.item_details',
|
<p><%= t('.body.item_details',
|
||||||
START: I18n.l(@attached_object.start_at, format: :long),
|
START: I18n.l(@attached_object.start_at, format: :long),
|
||||||
END:(I18n.l @attached_object.end_at, format: :hour_minute),
|
END: I18n.l(@attached_object.end_at, format: :hour_minute),
|
||||||
RESERVABLE: @attached_object.reservation.reservable.name) %>
|
RESERVABLE: @attached_object.reservation.reservable.name) %>
|
||||||
</p>
|
</p>
|
||||||
<p><%= t('.body.generate_refund') %></p>
|
<p><%= t('.body.generate_refund') %></p>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||||
|
|
||||||
<p><%= t('.body.slot_modified', NAME: @attached_object.reservation.user&.profile&.full_name || t('api.notifications.deleted_user')) %></p>
|
<p><%= t('.body.slot_modified', NAME: @attached_object.reservation.user&.profile&.full_name || t('api.notifications.deleted_user')) %></p>
|
||||||
<p><%= t('.body.new_date') %> <%= "#{I18n.l @attached_object.start_at, format: :long} - #{I18n.l @attached_object.end_at, format: :hour_minute}" %></p>
|
<p><%= t('.body.new_date') %> <%= "#{I18n.l(@attached_object.start_at, format: :long)} - #{I18n.l(@attached_object.end_at, format: :hour_minute)}" %></p>
|
||||||
<p><small><%= t('.body.old_date') %> <%= "#{I18n.l @attached_object.ex_start_at, format: :long} - #{I18n.l @attached_object.ex_end_at, format: :hour_minute}" %></small></p>
|
<p><small><%= t('.body.old_date') %> <%= "#{I18n.l(@attached_object.ex_start_at, format: :long)} - #{I18n.l(@attached_object.ex_end_at, format: :hour_minute)}" %></small></p>
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
<p>
|
<p>
|
||||||
<%= t('.body.remember',
|
<%= t('.body.remember',
|
||||||
REFERENCE: @attached_object.payment_schedule.reference,
|
REFERENCE: @attached_object.payment_schedule.reference,
|
||||||
AMOUNT: number_to_currency(@attached_object.amount),
|
AMOUNT: number_to_currency(@attached_object.amount / 100.00),
|
||||||
DATE: I18n.l @attached_object.due_date, format: :long) %>
|
DATE: I18n.l(@attached_object.due_date, format: :long)) %>
|
||||||
<%= t('.body.error') %>
|
<%= t('.body.error') %>
|
||||||
</p>
|
</p>
|
||||||
<p><%= t('.body.action') %></p>
|
<p><%= t('.body.action', DASHBOARD: link_to(t('.body.your_dashboard'), "#{root_url}#!/dashboard/invoices")) %></p>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||||
|
|
||||||
<p><%= t('.body.reservation_canceled', RESERVABLE: @attached_object.reservation.reservable.name ) %></p>
|
<p><%= t('.body.reservation_canceled', RESERVABLE: @attached_object.reservation.reservable.name ) %></p>
|
||||||
<p><%= "#{I18n.l @attached_object.start_at, format: :long} - #{I18n.l @attached_object.end_at, format: :hour_minute}" %></p>
|
<p><%= "#{I18n.l(@attached_object.start_at, format: :long)} - #{I18n.l(@attached_object.end_at, format: :hour_minute)}" %></p>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||||
|
|
||||||
<p><%= t('.body.reservation_changed_to') %></p>
|
<p><%= t('.body.reservation_changed_to') %></p>
|
||||||
<p><%= "#{I18n.l @attached_object.start_at, format: :long} - #{I18n.l @attached_object.end_at, format: :hour_minute}" %></p>
|
<p><%= "#{I18n.l(@attached_object.start_at, format: :long)} - #{I18n.l(@attached_object.end_at, format: :hour_minute)}" %></p>
|
||||||
<p><small><%= t('.body.previous_date') %> <%= "#{I18n.l @attached_object.ex_start_at, format: :long} - #{I18n.l @attached_object.ex_end_at, format: :hour_minute}" %></small></p>
|
<p><small><%= t('.body.previous_date') %> <%= "#{I18n.l(@attached_object.ex_start_at, format: :long)} - #{I18n.l(@attached_object.ex_end_at, format: :hour_minute)}" %></small></p>
|
||||||
|
@ -5,38 +5,49 @@
|
|||||||
class PaymentScheduleItemWorker
|
class PaymentScheduleItemWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
def perform
|
def perform(record_id = nil)
|
||||||
PaymentScheduleItem.where(state: 'new').where('due_date < ?', DateTime.current.end_of_day).each do |psi|
|
if record_id
|
||||||
# the following depends on the payment method (stripe/check)
|
psi = PaymentScheduleItem.find(record_id)
|
||||||
if psi.payment_schedule.payment_method == 'stripe'
|
check_item(psi)
|
||||||
### Stripe
|
else
|
||||||
stripe_key = Setting.get('stripe_secret_key')
|
PaymentScheduleItem.where.not(state: 'paid').where('due_date < ?', DateTime.current).each do |psi|
|
||||||
stp_suscription = Stripe::Subscription.retrieve(psi.payment_schedule.stp_subscription_id, api_key: stripe_key)
|
check_item(psi)
|
||||||
stp_invoice = Stripe::Invoice.retrieve(stp_suscription.latest_invoice, api_key: stripe_key)
|
|
||||||
if stp_invoice.status == 'paid'
|
|
||||||
##### Stripe / Successfully paid
|
|
||||||
PaymentScheduleService.new.generate_invoice(psi, stp_invoice)
|
|
||||||
psi.update_attributes(state: 'paid', payment_method: 'stripe', stp_invoice_id: stp_invoice.id)
|
|
||||||
elsif stp_suscription.status == 'past_due'
|
|
||||||
##### Stripe / Payment error
|
|
||||||
NotificationCenter.call type: 'notify_admin_payment_schedule_failed',
|
|
||||||
receiver: User.admins_and_managers,
|
|
||||||
attached_object: psi
|
|
||||||
NotificationCenter.call type: 'notify_member_payment_schedule_failed',
|
|
||||||
receiver: psi.payment_schedule.user,
|
|
||||||
attached_object: psi
|
|
||||||
stp_payment_intent = Stripe::PaymentIntent.retrieve(stp_invoice.payment_intent, api_key: stripe_key)
|
|
||||||
psi.update_attributes(state: stp_payment_intent.status, stp_invoice_id: stp_invoice.id)
|
|
||||||
else
|
|
||||||
psi.update_attributes(state: 'error')
|
|
||||||
end
|
|
||||||
else
|
|
||||||
### Check
|
|
||||||
NotificationCenter.call type: 'notify_admin_payment_schedule_check_deadline',
|
|
||||||
receiver: User.admins_and_managers,
|
|
||||||
attached_object: psi
|
|
||||||
psi.update_attributes(state: 'pending')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_item(psi)
|
||||||
|
# the following depends on the payment method (stripe/check)
|
||||||
|
if psi.payment_schedule.payment_method == 'stripe'
|
||||||
|
### Stripe
|
||||||
|
stripe_key = Setting.get('stripe_secret_key')
|
||||||
|
stp_subscription = Stripe::Subscription.retrieve(psi.payment_schedule.stp_subscription_id, api_key: stripe_key)
|
||||||
|
stp_invoice = Stripe::Invoice.retrieve(stp_subscription.latest_invoice, api_key: stripe_key)
|
||||||
|
if stp_invoice.status == 'paid'
|
||||||
|
##### Stripe / Successfully paid
|
||||||
|
PaymentScheduleService.new.generate_invoice(psi, stp_invoice)
|
||||||
|
psi.update_attributes(state: 'paid', payment_method: 'stripe', stp_invoice_id: stp_invoice.id)
|
||||||
|
elsif stp_subscription.status == 'past_due' || stp_invoice.status == 'open'
|
||||||
|
##### Stripe / Payment error
|
||||||
|
NotificationCenter.call type: 'notify_admin_payment_schedule_failed',
|
||||||
|
receiver: User.admins_and_managers,
|
||||||
|
attached_object: psi
|
||||||
|
NotificationCenter.call type: 'notify_member_payment_schedule_failed',
|
||||||
|
receiver: psi.payment_schedule.user,
|
||||||
|
attached_object: psi
|
||||||
|
stp_payment_intent = Stripe::PaymentIntent.retrieve(stp_invoice.payment_intent, api_key: stripe_key)
|
||||||
|
psi.update_attributes(state: stp_payment_intent.status,
|
||||||
|
stp_invoice_id: stp_invoice.id,
|
||||||
|
client_secret: stp_payment_intent.client_secret)
|
||||||
|
else
|
||||||
|
psi.update_attributes(state: 'error')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
### Check
|
||||||
|
NotificationCenter.call type: 'notify_admin_payment_schedule_check_deadline',
|
||||||
|
receiver: User.admins_and_managers,
|
||||||
|
attached_object: psi
|
||||||
|
psi.update_attributes(state: 'pending')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -667,6 +667,8 @@ en:
|
|||||||
confirm_check_cashing: "Confirm the cashing of the check"
|
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_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_button: "Confirm"
|
confirm_button: "Confirm"
|
||||||
|
resolve_action: "Resolve the action"
|
||||||
|
ok_button: "OK"
|
||||||
document_filters:
|
document_filters:
|
||||||
reference: "Reference"
|
reference: "Reference"
|
||||||
customer: "Customer"
|
customer: "Customer"
|
||||||
|
@ -667,6 +667,8 @@ fr:
|
|||||||
confirm_check_cashing: "Confirmer l'encaissement du chèque"
|
confirm_check_cashing: "Confirmer l'encaissement du chèque"
|
||||||
confirm_check_cashing_body: "Vous devez encaisser un chèque de {AMOUNT} pour l'échéance du {DATE}. En confirmant l'encaissement du chèque, une facture sera générée pour cette échéance."
|
confirm_check_cashing_body: "Vous devez encaisser un chèque de {AMOUNT} pour l'échéance du {DATE}. En confirmant l'encaissement du chèque, une facture sera générée pour cette échéance."
|
||||||
confirm_button: "Confirmer"
|
confirm_button: "Confirmer"
|
||||||
|
resolve_action: "Résoudre l'action"
|
||||||
|
ok_button: "OK"
|
||||||
document_filters:
|
document_filters:
|
||||||
reference: "Référence"
|
reference: "Référence"
|
||||||
customer: "Client"
|
customer: "Client"
|
||||||
|
@ -474,3 +474,7 @@ en:
|
|||||||
what_to_do: "What do you want to do?"
|
what_to_do: "What do you want to do?"
|
||||||
tour: "Start the feature tour"
|
tour: "Start the feature tour"
|
||||||
guide: "Open the user's manual"
|
guide: "Open the user's manual"
|
||||||
|
# 2nd factor authentication for card payments
|
||||||
|
stripe_confirm:
|
||||||
|
pending: "Pending for action..."
|
||||||
|
success: "Thank you, your card setup is complete. The payment will be proceeded shortly."
|
||||||
|
@ -474,3 +474,7 @@ fr:
|
|||||||
what_to_do: "Que voulez-vous faire ?"
|
what_to_do: "Que voulez-vous faire ?"
|
||||||
tour: "Lancer la visite guidée"
|
tour: "Lancer la visite guidée"
|
||||||
guide: "Ouvrir le manuel de l'utilisateur"
|
guide: "Ouvrir le manuel de l'utilisateur"
|
||||||
|
# 2nd factor authentication for card payments
|
||||||
|
stripe_confirm:
|
||||||
|
pending: "En attente de l'action ..."
|
||||||
|
success: "Merci, la configuration de votre carte est terminée. Le paiement sera effectué sous peu."
|
||||||
|
@ -272,6 +272,7 @@ en:
|
|||||||
subject: "A refund has been generated"
|
subject: "A refund has been generated"
|
||||||
body:
|
body:
|
||||||
refund_created: "A refund of %{AMOUNT} has been generated on invoice %{INVOICE} of user %{USER}"
|
refund_created: "A refund of %{AMOUNT} has been generated on invoice %{INVOICE} of user %{USER}"
|
||||||
|
wallet_refund_created: "A refund of %{AMOUNT} has been generated for the credit of the wallet of user %{USER}"
|
||||||
download: "Click here to download this refund invoice"
|
download: "Click here to download this refund invoice"
|
||||||
notify_admins_role_update:
|
notify_admins_role_update:
|
||||||
subject: "The role of a user has changed"
|
subject: "The role of a user has changed"
|
||||||
@ -294,17 +295,18 @@ en:
|
|||||||
schedule_in_your_dashboard_html: "You can find this payment schedule at any time from %{DASHBOARD} on the Fab Lab's website."
|
schedule_in_your_dashboard_html: "You can find this payment schedule at any time from %{DASHBOARD} on the Fab Lab's website."
|
||||||
your_dashboard: "your dashboard"
|
your_dashboard: "your dashboard"
|
||||||
notify_admin_payment_schedule_failed:
|
notify_admin_payment_schedule_failed:
|
||||||
subject: "Card debit failure"
|
subject: "[URGENT] Card debit failure"
|
||||||
body:
|
body:
|
||||||
remember: "In accordance with the %{REFERENCE} payment schedule, a debit by card of %{AMOUNT} was scheduled on %{DATE}."
|
remember: "In accordance with the %{REFERENCE} payment schedule, a debit by card of %{AMOUNT} was scheduled on %{DATE}."
|
||||||
error: "Unfortunately, this card debit was unable to complete successfully."
|
error: "Unfortunately, this card debit was unable to complete successfully."
|
||||||
action: "Please go to your payment schedule management interface as soon as possible to resolve the problem."
|
action: "Please contact the member as soon as possible, then go to the payment schedule management interface to resolve the problem. After about 24 hours, the card subscription will be cancelled."
|
||||||
notify_member_payment_schedule_failed:
|
notify_member_payment_schedule_failed:
|
||||||
subject: "Card debit failure"
|
subject: "[URGENT] Card debit failure"
|
||||||
body:
|
body:
|
||||||
remember: "In accordance with your %{REFERENCE} payment schedule, a debit by card of %{AMOUNT} was scheduled on %{DATE}."
|
remember: "In accordance with your %{REFERENCE} payment schedule, a debit by card of %{AMOUNT} was scheduled on %{DATE}."
|
||||||
error: "Unfortunately, this card debit was unable to complete successfully."
|
error: "Unfortunately, this card debit was unable to complete successfully."
|
||||||
action: "Please contact the manager of your FabLab as soon as possible, otherwise your subscription may be interrupted."
|
action: "Please check %{DASHBOARD} or contact a manager before 24 hours, otherwise your subscription may be interrupted."
|
||||||
|
your_dashboard: "your dashboard"
|
||||||
notify_admin_payment_schedule_check_deadline:
|
notify_admin_payment_schedule_check_deadline:
|
||||||
subject: "Payment deadline"
|
subject: "Payment deadline"
|
||||||
body:
|
body:
|
||||||
|
@ -272,6 +272,7 @@ fr:
|
|||||||
subject: "Un avoir a été généré"
|
subject: "Un avoir a été généré"
|
||||||
body:
|
body:
|
||||||
refund_created: "Un avoir de %{AMOUNT} a été généré sur la facture %{INVOICE} de l'utilisateur %{USER}"
|
refund_created: "Un avoir de %{AMOUNT} a été généré sur la facture %{INVOICE} de l'utilisateur %{USER}"
|
||||||
|
wallet_refund_created: "Un avoir de %{AMOUNT} a été généré pour le crédit du porte-monnaie de l'utilisateur %{USER}"
|
||||||
download: "Cliquez ici pour télécharger cet avoir"
|
download: "Cliquez ici pour télécharger cet avoir"
|
||||||
notify_admins_role_update:
|
notify_admins_role_update:
|
||||||
subject: "Le rôle d'un utilisateur a changé"
|
subject: "Le rôle d'un utilisateur a changé"
|
||||||
@ -294,17 +295,18 @@ fr:
|
|||||||
schedule_in_your_dashboard_html: "Vous pouvez à tout moment retrouver votre échéancier dans %{DASHBOARD} sur le site du Fab Lab."
|
schedule_in_your_dashboard_html: "Vous pouvez à tout moment retrouver votre échéancier dans %{DASHBOARD} sur le site du Fab Lab."
|
||||||
your_dashboard: "votre tableau de bord"
|
your_dashboard: "votre tableau de bord"
|
||||||
notify_admin_payment_schedule_failed:
|
notify_admin_payment_schedule_failed:
|
||||||
subject: "Échec du prélèvement par carte"
|
subject: "[URGENT] Échec du prélèvement par carte"
|
||||||
body:
|
body:
|
||||||
remember: "Conformément à l'échéancier de paiement %{REFERENCE}, un prélèvement par carte de %{AMOUNT} était prévu le %{DATE}."
|
remember: "Conformément à l'échéancier de paiement %{REFERENCE}, un prélèvement par carte de %{AMOUNT} était prévu le %{DATE}."
|
||||||
error: "Malheureusement, ce prélèvement n'a pas pu être effectué correctement."
|
error: "Malheureusement, ce prélèvement n'a pas pu être effectué correctement."
|
||||||
action: "Veuillez vous rendre au plus tôt dans votre interface de gestion des échéanciers pour régler le problème."
|
action: "Veuillez vous mettre en relation avec le membre au plus tôt, puis vous rendre dans l'interface de gestion des échéanciers afin de régler le problème. Au delà d'environ 24 heures, l'abonnement par carte bancaire sera annulé."
|
||||||
notify_member_payment_schedule_failed:
|
notify_member_payment_schedule_failed:
|
||||||
subject: "Échec du prélèvement par carte"
|
subject: "[URGENT] Échec du prélèvement par carte"
|
||||||
body:
|
body:
|
||||||
remember: "Conformément à votre échéancier de paiement %{REFERENCE}, un prélèvement par carte de %{AMOUNT} était prévu le %{DATE}."
|
remember: "Conformément à votre échéancier de paiement %{REFERENCE}, un prélèvement par carte de %{AMOUNT} était prévu le %{DATE}."
|
||||||
error: "Malheureusement, ce prélèvement n'a pas pu être effectué correctement."
|
error: "Malheureusement, ce prélèvement n'a pas pu être effectué correctement."
|
||||||
action: "Veuillez prendre contact avec le gestionnaire de votre FabLab au plus tôt, faute de quoi votre abonnement risque d'être interrompu."
|
action: "Veuillez vous rendre dans votre %{DASHBOARD} ou prendre contact avec un gestionnaire sous 24 heures, faute de quoi votre abonnement risque d'être interrompu."
|
||||||
|
your_dashboard: "votre tableau de bord"
|
||||||
notify_admin_payment_schedule_check_deadline:
|
notify_admin_payment_schedule_check_deadline:
|
||||||
subject: "Échéance d'encaissement"
|
subject: "Échéance d'encaissement"
|
||||||
body:
|
body:
|
||||||
|
@ -115,6 +115,7 @@ Rails.application.routes.draw do
|
|||||||
post 'list', action: 'list', on: :collection
|
post 'list', action: 'list', on: :collection
|
||||||
get 'download', on: :member
|
get 'download', on: :member
|
||||||
post 'items/:id/cash_check', action: 'cash_check', on: :collection
|
post 'items/:id/cash_check', action: 'cash_check', on: :collection
|
||||||
|
post 'items/:id/refresh_item', action: 'refresh_item', on: :collection
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :i_calendar, only: %i[index create destroy] do
|
resources :i_calendar, only: %i[index create destroy] do
|
||||||
|
@ -48,7 +48,7 @@ version_check:
|
|||||||
queue: system
|
queue: system
|
||||||
|
|
||||||
payment_schedule_item:
|
payment_schedule_item:
|
||||||
cron: "0 23 * * *" # every day at 11pm
|
cron: "0 * * * *" # every day, every hour
|
||||||
class: 'PaymentScheduleItemWorker'
|
class: 'PaymentScheduleItemWorker'
|
||||||
queue: default
|
queue: default
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ class CreatePaymentScheduleItems < ActiveRecord::Migration[5.2]
|
|||||||
t.jsonb :details, default: '{}'
|
t.jsonb :details, default: '{}'
|
||||||
t.string :stp_invoice_id
|
t.string :stp_invoice_id
|
||||||
t.string :payment_method
|
t.string :payment_method
|
||||||
|
t.string :client_secret
|
||||||
t.belongs_to :payment_schedule, foreign_key: true
|
t.belongs_to :payment_schedule, foreign_key: true
|
||||||
t.belongs_to :invoice, foreign_key: true
|
t.belongs_to :invoice, foreign_key: true
|
||||||
t.string :footprint
|
t.string :footprint
|
||||||
|
@ -1474,6 +1474,7 @@ CREATE TABLE public.payment_schedule_items (
|
|||||||
details jsonb DEFAULT '"{}"'::jsonb,
|
details jsonb DEFAULT '"{}"'::jsonb,
|
||||||
stp_invoice_id character varying,
|
stp_invoice_id character varying,
|
||||||
payment_method character varying,
|
payment_method character varying,
|
||||||
|
client_secret character varying,
|
||||||
payment_schedule_id bigint,
|
payment_schedule_id bigint,
|
||||||
invoice_id bigint,
|
invoice_id bigint,
|
||||||
footprint character varying,
|
footprint character varying,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user