mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-30 19:52:20 +01:00
Merge branch 'dev' into dependabot/npm_and_yarn/webpack-5.76.0
This commit is contained in:
commit
b426a3988a
@ -1,5 +1,8 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
- [TODO DEPLOY] `rails db:seed`
|
||||
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`
|
||||
|
||||
## v5.8.2 2023 March 13
|
||||
|
||||
- Improved upgrade script
|
||||
|
@ -8,6 +8,8 @@ class API::LocalPaymentController < API::PaymentsController
|
||||
|
||||
authorize LocalPaymentContext.new(cart, price[:amount])
|
||||
|
||||
render json: cart.errors, status: :unprocessable_entity and return unless cart.valid?
|
||||
|
||||
render on_payment_success(nil, nil, cart)
|
||||
end
|
||||
|
||||
|
@ -55,7 +55,7 @@ class API::PayzenController < API::PaymentsController
|
||||
|
||||
def check_cart
|
||||
cart = shopping_cart
|
||||
render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid?
|
||||
render json: cart.errors, status: :unprocessable_entity and return unless cart.valid?
|
||||
|
||||
render json: { cart: 'ok' }, status: :ok
|
||||
end
|
||||
|
@ -80,11 +80,13 @@ class API::PlansController < API::ApiController
|
||||
end
|
||||
|
||||
@parameters = @parameters.require(:plan)
|
||||
.permit(:base_name, :type, :group_id, :amount, :interval, :interval_count, :is_rolling,
|
||||
.permit(:base_name, :type, :group_id, :amount, :interval, :interval_count, :is_rolling, :limiting,
|
||||
:training_credit_nb, :ui_weight, :disabled, :monthly_payment, :description, :plan_category_id,
|
||||
:machines_visibility,
|
||||
plan_file_attributes: %i[id attachment _destroy],
|
||||
prices_attributes: %i[id amount],
|
||||
advanced_accounting_attributes: %i[code analytical_section])
|
||||
advanced_accounting_attributes: %i[code analytical_section],
|
||||
plan_limitations_attributes: %i[id limitable_id limitable_type limit _destroy])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -19,7 +19,7 @@ class API::StripeController < API::PaymentsController
|
||||
res = nil # json of the API answer
|
||||
|
||||
cart = shopping_cart
|
||||
render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid?
|
||||
render json: cart.errors, status: :unprocessable_entity and return unless cart.valid?
|
||||
|
||||
begin
|
||||
amount = debit_amount(cart) # will contains the amount and the details of each invoice lines
|
||||
@ -73,7 +73,7 @@ class API::StripeController < API::PaymentsController
|
||||
|
||||
def setup_subscription
|
||||
cart = shopping_cart
|
||||
render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid?
|
||||
render json: cart.errors, status: :unprocessable_entity and return unless cart.valid?
|
||||
|
||||
service = Stripe::Service.new
|
||||
method = service.attach_method_as_default(
|
||||
|
@ -5,24 +5,34 @@ import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from './fab-button';
|
||||
import { FabModal } from './fab-modal';
|
||||
|
||||
interface EditDestroyButtonsProps {
|
||||
onDeleteSuccess: (message: string) => void,
|
||||
type EditDestroyButtonsCommon = {
|
||||
onError: (message: string) => void,
|
||||
onEdit: () => void,
|
||||
itemId: number,
|
||||
itemType: string,
|
||||
apiDestroy: (itemId: number) => Promise<void>,
|
||||
confirmationMessage?: string|ReactNode,
|
||||
destroy: (itemId: number) => Promise<void>,
|
||||
className?: string,
|
||||
iconSize?: number
|
||||
iconSize?: number,
|
||||
showEditButton?: boolean,
|
||||
}
|
||||
|
||||
type DeleteSuccess =
|
||||
{ onDeleteSuccess: (message: string) => void, deleteSuccessMessage: string } |
|
||||
{ onDeleteSuccess?: never, deleteSuccessMessage?: never }
|
||||
|
||||
type DestroyMessages =
|
||||
({ showDestroyConfirmation?: true } &
|
||||
({ itemType: string, confirmationTitle?: string, confirmationMessage?: string|ReactNode } |
|
||||
{ itemType?: never, confirmationTitle: string, confirmationMessage: string|ReactNode })) |
|
||||
{ showDestroyConfirmation: false, itemType?: never, confirmationTitle?: never, confirmationMessage?: never };
|
||||
|
||||
type EditDestroyButtonsProps = EditDestroyButtonsCommon & DeleteSuccess & DestroyMessages;
|
||||
|
||||
/**
|
||||
* This component shows a group of two buttons.
|
||||
* Destroy : shows a modal dialog to ask the user for confirmation about the deletion of the provided item.
|
||||
* Edit : triggers the provided function.
|
||||
*/
|
||||
export const EditDestroyButtons: React.FC<EditDestroyButtonsProps> = ({ onDeleteSuccess, onError, onEdit, itemId, itemType, apiDestroy, confirmationMessage, className, iconSize = 20 }) => {
|
||||
export const EditDestroyButtons: React.FC<EditDestroyButtonsProps> = ({ onDeleteSuccess, onError, onEdit, itemId, itemType, destroy, confirmationTitle, confirmationMessage, deleteSuccessMessage, className, iconSize = 20, showEditButton = true, showDestroyConfirmation = true }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [deletionModal, setDeletionModal] = useState<boolean>(false);
|
||||
@ -34,30 +44,41 @@ export const EditDestroyButtons: React.FC<EditDestroyButtonsProps> = ({ onDelete
|
||||
setDeletionModal(!deletionModal);
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on the 'destroy' button
|
||||
*/
|
||||
const handleDestroyRequest = (): void => {
|
||||
if (showDestroyConfirmation) {
|
||||
toggleDeletionModal();
|
||||
} else {
|
||||
onDeleteConfirmed();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The deletion has been confirmed by the user.
|
||||
* Call the API to trigger the deletion of the given item
|
||||
*/
|
||||
const onDeleteConfirmed = (): void => {
|
||||
apiDestroy(itemId).then(() => {
|
||||
onDeleteSuccess(t('app.admin.edit_destroy_buttons.deleted', { TYPE: itemType }));
|
||||
destroy(itemId).then(() => {
|
||||
typeof onDeleteSuccess === 'function' && onDeleteSuccess(deleteSuccessMessage || t('app.admin.edit_destroy_buttons.deleted'));
|
||||
}).catch((error) => {
|
||||
onError(t('app.admin.edit_destroy_buttons.unable_to_delete', { TYPE: itemType }) + error);
|
||||
onError(t('app.admin.edit_destroy_buttons.unable_to_delete') + error);
|
||||
});
|
||||
toggleDeletionModal();
|
||||
setDeletionModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`edit-destroy-buttons ${className || ''}`}>
|
||||
<FabButton className='edit-btn' onClick={onEdit}>
|
||||
{showEditButton && <FabButton className='edit-btn' onClick={onEdit}>
|
||||
<PencilSimple size={iconSize} weight="fill" />
|
||||
</FabButton>
|
||||
<FabButton type='button' className='delete-btn' onClick={toggleDeletionModal}>
|
||||
</FabButton>}
|
||||
<FabButton type='button' className='delete-btn' onClick={handleDestroyRequest}>
|
||||
<Trash size={iconSize} weight="fill" />
|
||||
</FabButton>
|
||||
</div>
|
||||
<FabModal title={t('app.admin.edit_destroy_buttons.delete_item', { TYPE: itemType })}
|
||||
<FabModal title={confirmationTitle || t('app.admin.edit_destroy_buttons.delete_item', { TYPE: itemType })}
|
||||
isOpen={deletionModal}
|
||||
toggleModal={toggleDeletionModal}
|
||||
closeButton={true}
|
||||
|
@ -58,10 +58,10 @@ export const FormSelect = <TFieldValues extends FieldValues, TContext extends ob
|
||||
<AbstractSelect ref={ref}
|
||||
classNamePrefix="rs"
|
||||
className="rs"
|
||||
value={options.find(c => c.value === value)}
|
||||
value={value === null ? null : options.find(c => c.value === value)}
|
||||
onChange={val => {
|
||||
onChangeCb(val.value);
|
||||
onChange(val.value);
|
||||
onChangeCb(val?.value);
|
||||
onChange(val?.value);
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
isDisabled={isDisabled}
|
||||
|
@ -0,0 +1,74 @@
|
||||
import { FieldArrayWithId } from 'react-hook-form/dist/types/fieldArray';
|
||||
import { UseFormRegister } from 'react-hook-form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { X } from 'phosphor-react';
|
||||
import { FormInput } from './form-input';
|
||||
import { FieldArrayPath } from 'react-hook-form/dist/types/path';
|
||||
|
||||
interface FormUnsavedListProps<TFieldValues, TFieldArrayName extends FieldArrayPath<TFieldValues>, TKeyName extends string> {
|
||||
fields: Array<FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>>,
|
||||
onRemove: (index: number) => void,
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
className?: string,
|
||||
title: string,
|
||||
shouldRenderField?: (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>) => boolean,
|
||||
renderField: (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>) => ReactNode,
|
||||
formAttributeName: `${string}_attributes`,
|
||||
formAttributes: Array<keyof FieldArrayWithId<TFieldValues, TFieldArrayName>>,
|
||||
saveReminderLabel?: string | ReactNode,
|
||||
cancelLabel?: string | ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* This component render a list of unsaved attributes, created elsewhere than in the form (e.g. in a modal dialog)
|
||||
* and pending for the form to be saved.
|
||||
*
|
||||
* The `renderField` attribute should return a JSX element composed like the following example:
|
||||
* ```
|
||||
* <> <!-- empty tag -->
|
||||
* <div className="group"> <!-- the group class is important -->
|
||||
* <span>Attribute 1</span> <!-- a span tag for the title -->
|
||||
* <p>{item.attr1}</p> <!-- a paragraph tag for the value -->
|
||||
* </div>
|
||||
* <div className="group">
|
||||
* ...
|
||||
* </div>
|
||||
* </>
|
||||
* ```
|
||||
*/
|
||||
export const FormUnsavedList = <TFieldValues extends FieldValues = FieldValues, TFieldArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>, TKeyName extends string = 'id'>({ fields, onRemove, register, className, title, shouldRenderField = () => true, renderField, formAttributeName, formAttributes, saveReminderLabel, cancelLabel }: FormUnsavedListProps<TFieldValues, TFieldArrayName, TKeyName>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
/**
|
||||
* Render an unsaved field
|
||||
*/
|
||||
const renderUnsavedField = (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>, index: number): ReactNode => {
|
||||
return (
|
||||
<div key={index} className="unsaved-field">
|
||||
{renderField(field)}
|
||||
<p className="cancel-action" onClick={() => onRemove(index)}>
|
||||
{cancelLabel || t('app.shared.form_unsaved_list.cancel')}
|
||||
<X size={20} />
|
||||
</p>
|
||||
{formAttributes.map((attribute, attrIndex) => (
|
||||
<FormInput key={attrIndex} id={`${formAttributeName}.${index}.${attribute}`} register={register} type="hidden" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (fields.filter(shouldRenderField).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={`form-unsaved-list ${className || ''}`}>
|
||||
<span className="title">{title}</span>
|
||||
<span className="save-notice">{saveReminderLabel || t('app.shared.form_unsaved_list.save_reminder')}</span>
|
||||
{fields.map((field, index) => {
|
||||
if (!shouldRenderField(field)) return false;
|
||||
return renderUnsavedField(field, index);
|
||||
}).filter(Boolean)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -124,7 +124,7 @@ export const MachineCategoriesList: React.FC<MachineCategoriesListProps> = ({ on
|
||||
onEdit={editMachineCategory(category)}
|
||||
itemId={category.id}
|
||||
itemType={t('app.admin.machine_categories_list.machine_category')}
|
||||
apiDestroy={MachineCategoryAPI.destroy} />
|
||||
destroy={MachineCategoryAPI.destroy} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -24,6 +24,10 @@ import { UserPlus } from 'phosphor-react';
|
||||
import { PartnerModal } from './partner-modal';
|
||||
import { PlanPricingForm } from './plan-pricing-form';
|
||||
import { AdvancedAccountingForm } from '../accounting/advanced-accounting-form';
|
||||
import { FabTabs } from '../base/fab-tabs';
|
||||
import { PlanLimitForm } from './plan-limit-form';
|
||||
import { UnsavedFormAlert } from '../form/unsaved-form-alert';
|
||||
import { UIRouter } from '@uirouter/angularjs';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -33,13 +37,14 @@ interface PlanFormProps {
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
beforeSubmit?: (data: Plan) => void,
|
||||
uiRouter: UIRouter
|
||||
}
|
||||
|
||||
/**
|
||||
* Form to edit or create subscription plans
|
||||
*/
|
||||
export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuccess, beforeSubmit }) => {
|
||||
const { handleSubmit, register, control, formState, setValue } = useForm<Plan>({ defaultValues: { ...plan } });
|
||||
export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuccess, beforeSubmit, uiRouter }) => {
|
||||
const { handleSubmit, register, control, formState, setValue, getValues, resetField } = useForm<Plan>({ defaultValues: { ...plan } });
|
||||
const output = useWatch<Plan>({ control }); // eslint-disable-line
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
@ -51,13 +56,19 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
|
||||
|
||||
useEffect(() => {
|
||||
GroupAPI.index({ disabled: false })
|
||||
.then(res => setGroups(res.map(g => { return { value: g.id, label: g.name }; })))
|
||||
.then(res => setGroups(res.map(g => {
|
||||
return { value: g.id, label: g.name };
|
||||
})))
|
||||
.catch(onError);
|
||||
PlanCategoryAPI.index()
|
||||
.then(res => setCategories(res.map(c => { return { value: c.id, label: c.name }; })))
|
||||
.then(res => setCategories(res.map(c => {
|
||||
return { value: c.id, label: c.name };
|
||||
})))
|
||||
.catch(onError);
|
||||
UserAPI.index({ role: 'partner' })
|
||||
.then(res => setPartners(res.map(p => { return { value: p.id, label: p.name }; })))
|
||||
.then(res => setPartners(res.map(p => {
|
||||
return { value: p.id, label: p.name };
|
||||
})))
|
||||
.catch(onError);
|
||||
}, []);
|
||||
|
||||
@ -101,7 +112,9 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
|
||||
* Return the available options for the plan period
|
||||
*/
|
||||
const buildPeriodsOptions = (): Array<SelectOption<string>> => {
|
||||
return ['week', 'month', 'year'].map(d => { return { value: d, label: t(`app.admin.plan_form.${d}`) }; });
|
||||
return ['week', 'month', 'year'].map(d => {
|
||||
return { value: d, label: t(`app.admin.plan_form.${d}`) };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -130,140 +143,225 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
|
||||
setValue('partner_id', user.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="plan-form">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<h4>{t('app.admin.plan_form.general_information')}</h4>
|
||||
<FormInput register={register}
|
||||
id="base_name"
|
||||
formState={formState}
|
||||
rules={{
|
||||
required: true,
|
||||
maxLength: { value: 24, message: t('app.admin.plan_form.name_max_length') }
|
||||
}}
|
||||
label={t('app.admin.plan_form.name')} />
|
||||
{action === 'create' && <FormSwitch control={control}
|
||||
formState={formState}
|
||||
onChange={handleAllGroupsChange}
|
||||
defaultValue={false}
|
||||
label={t('app.admin.plan_form.transversal')}
|
||||
tooltip={t('app.admin.plan_form.transversal_help')}
|
||||
id="all_groups" />}
|
||||
{!allGroups && groups && <FormSelect options={groups}
|
||||
formState={formState}
|
||||
control={control}
|
||||
rules={{ required: !allGroups }}
|
||||
disabled={action === 'update'}
|
||||
label={t('app.admin.plan_form.group')}
|
||||
id="group_id" />}
|
||||
{categories?.length > 0 && <FormSelect options={categories}
|
||||
formState={formState}
|
||||
control={control}
|
||||
id="plan_category_id"
|
||||
tooltip={t('app.admin.plan_form.category_help')}
|
||||
label={t('app.admin.plan_form.category')} />}
|
||||
{action === 'update' && <FabAlert level="warning">
|
||||
{t('app.admin.plan_form.edit_amount_info')}
|
||||
</FabAlert>}
|
||||
<FormInput register={register}
|
||||
formState={formState}
|
||||
id="amount"
|
||||
type="number"
|
||||
step={0.01}
|
||||
addOn={FormatLib.currencySymbol()}
|
||||
rules={{ required: true, min: 0 }}
|
||||
label={t('app.admin.plan_form.subscription_price')} />
|
||||
<FormInput register={register}
|
||||
formState={formState}
|
||||
id="ui_weight"
|
||||
type="number"
|
||||
label={t('app.admin.plan_form.visual_prominence')}
|
||||
tooltip={t('app.admin.plan_form.visual_prominence_help')} />
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
id="is_rolling"
|
||||
label={t('app.admin.plan_form.rolling_subscription')}
|
||||
disabled={action === 'update'}
|
||||
tooltip={t('app.admin.plan_form.rolling_subscription_help')} />
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
id="monthly_payment"
|
||||
label={t('app.admin.plan_form.monthly_payment')}
|
||||
disabled={action === 'update' || output.interval === 'week'}
|
||||
tooltip={t('app.admin.plan_form.monthly_payment_help')} />
|
||||
<FormRichText control={control}
|
||||
formState={formState}
|
||||
id="description"
|
||||
label={t('app.admin.plan_form.description')}
|
||||
limit={200}
|
||||
heading link blockquote />
|
||||
<FormFileUpload setValue={setValue}
|
||||
register={register}
|
||||
formState={formState}
|
||||
defaultFile={output.plan_file_attributes}
|
||||
id="plan_file_attributes"
|
||||
className="plan-sheet"
|
||||
label={t('app.admin.plan_form.information_sheet')} />
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
id="disabled"
|
||||
defaultValue={false}
|
||||
label={t('app.admin.plan_form.disabled')}
|
||||
tooltip={t('app.admin.plan_form.disabled_help')} />
|
||||
<h4>{t('app.admin.plan_form.duration')}</h4>
|
||||
<div className="duration">
|
||||
/**
|
||||
* Render the content of the 'subscriptions settings' tab
|
||||
*/
|
||||
const renderSettingsTab = () => (
|
||||
<div className="plan-form-content">
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_form.description')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
<FormInput register={register}
|
||||
rules={{ required: true, min: 1 }}
|
||||
disabled={action === 'update'}
|
||||
id="base_name"
|
||||
formState={formState}
|
||||
label={t('app.admin.plan_form.number_of_periods')}
|
||||
type="number"
|
||||
id="interval_count" />
|
||||
<FormSelect options={buildPeriodsOptions()}
|
||||
control={control}
|
||||
disabled={action === 'update'}
|
||||
onChange={handlePeriodUpdate}
|
||||
id="interval"
|
||||
label={t('app.admin.plan_form.period')}
|
||||
formState={formState}
|
||||
rules={{ required: true }} />
|
||||
rules={{
|
||||
required: true,
|
||||
maxLength: { value: 24, message: t('app.admin.plan_form.name_max_length') }
|
||||
}}
|
||||
label={t('app.admin.plan_form.name')} />
|
||||
<FormRichText control={control}
|
||||
formState={formState}
|
||||
id="description"
|
||||
label={t('app.admin.plan_form.description')}
|
||||
limit={200}
|
||||
heading link blockquote />
|
||||
<FormFileUpload setValue={setValue}
|
||||
register={register}
|
||||
formState={formState}
|
||||
defaultFile={output.plan_file_attributes}
|
||||
id="plan_file_attributes"
|
||||
className="plan-sheet"
|
||||
label={t('app.admin.plan_form.information_sheet')} />
|
||||
</div>
|
||||
<h4>{t('app.admin.plan_form.partnership')}</h4>
|
||||
<div className="partnership">
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_form.general_settings')}</p>
|
||||
<p className="description">{t('app.admin.plan_form.general_settings_info')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
{action === 'create' && <FormSwitch control={control}
|
||||
formState={formState}
|
||||
onChange={handleAllGroupsChange}
|
||||
defaultValue={false}
|
||||
label={t('app.admin.plan_form.transversal')}
|
||||
tooltip={t('app.admin.plan_form.transversal_help')}
|
||||
id="all_groups" />}
|
||||
{!allGroups && groups && <FormSelect options={groups}
|
||||
formState={formState}
|
||||
control={control}
|
||||
rules={{ required: !allGroups }}
|
||||
disabled={action === 'update'}
|
||||
label={t('app.admin.plan_form.group')}
|
||||
id="group_id" />}
|
||||
<div className="grp">
|
||||
<FormInput register={register}
|
||||
rules={{ required: true, min: 1 }}
|
||||
disabled={action === 'update'}
|
||||
formState={formState}
|
||||
label={t('app.admin.plan_form.number_of_periods')}
|
||||
type="number"
|
||||
id="interval_count" />
|
||||
<FormSelect options={buildPeriodsOptions()}
|
||||
control={control}
|
||||
disabled={action === 'update'}
|
||||
onChange={handlePeriodUpdate}
|
||||
id="interval"
|
||||
label={t('app.admin.plan_form.period')}
|
||||
formState={formState}
|
||||
rules={{ required: true }} />
|
||||
</div>
|
||||
{action === 'update' && <FabAlert level="info">
|
||||
{t('app.admin.plan_form.edit_amount_info')}
|
||||
</FabAlert>}
|
||||
<FormInput register={register}
|
||||
formState={formState}
|
||||
id="amount"
|
||||
type="number"
|
||||
step={0.01}
|
||||
addOn={FormatLib.currencySymbol()}
|
||||
rules={{ required: true, min: 0 }}
|
||||
label={t('app.admin.plan_form.subscription_price')} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_form.activation_and_payment')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
id="disabled"
|
||||
defaultValue={false}
|
||||
label={t('app.admin.plan_form.disabled')}
|
||||
tooltip={t('app.admin.plan_form.disabled_help')} />
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
id="is_rolling"
|
||||
label={t('app.admin.plan_form.rolling_subscription')}
|
||||
disabled={action === 'update'}
|
||||
tooltip={t('app.admin.plan_form.rolling_subscription_help')} />
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
id="monthly_payment"
|
||||
label={t('app.admin.plan_form.monthly_payment')}
|
||||
disabled={action === 'update' || output.interval === 'week'}
|
||||
tooltip={t('app.admin.plan_form.monthly_payment_help')} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_form.partnership')}</p>
|
||||
<p className="description">{t('app.admin.plan_form.partner_plan_help')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
<FormSwitch control={control}
|
||||
id="partnership"
|
||||
disabled={action === 'update'}
|
||||
tooltip={t('app.admin.plan_form.partner_plan_help')}
|
||||
defaultValue={plan?.type === 'PartnerPlan'}
|
||||
onChange={handlePartnershipChange}
|
||||
formState={formState}
|
||||
label={t('app.admin.plan_form.partner_plan')} />
|
||||
<FormInput register={register} type="hidden" id="type" defaultValue="Plan" />
|
||||
{output.type === 'PartnerPlan' && <div className="partner">
|
||||
<FabButton className="add-partner is-info" icon={<UserPlus size={20} />} onClick={tooglePartnerModal}>
|
||||
{t('app.admin.plan_form.new_user')}
|
||||
</FabButton>
|
||||
{partners && <FormSelect id="partner_id"
|
||||
options={partners}
|
||||
control={control}
|
||||
formState={formState}
|
||||
rules={{ required: output.type === 'PartnerPlan' }}
|
||||
tooltip={t('app.admin.plan_form.alert_partner_notification')}
|
||||
label={t('app.admin.plan_form.notified_partner')} />}
|
||||
{output.partner_id && <FabAlert level="info">
|
||||
{t('app.admin.plan_form.alert_partner_notification')}
|
||||
</FabAlert>}
|
||||
<FabButton className="is-secondary" icon={<UserPlus size={20} />} onClick={tooglePartnerModal}>
|
||||
{t('app.admin.plan_form.new_user')}
|
||||
</FabButton>
|
||||
</div>}
|
||||
</div>
|
||||
<AdvancedAccountingForm register={register} onError={onError} />
|
||||
{action === 'update' && <PlanPricingForm formState={formState}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_form.slots_visibility')}</p>
|
||||
<p className="description">{t('app.admin.plan_form.slots_visibility_help')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
<FormInput register={register}
|
||||
formState={formState}
|
||||
nullable
|
||||
id="machines_visibility"
|
||||
type="number"
|
||||
label={t('app.admin.plan_form.machines_visibility')} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_form.display')} </p>
|
||||
</header>
|
||||
<div className="content">
|
||||
{categories?.length > 0 && <FormSelect options={categories}
|
||||
formState={formState}
|
||||
control={control}
|
||||
onError={onError}
|
||||
setValue={setValue}
|
||||
register={register} />}
|
||||
<FabButton type="submit" className="is-info submit-btn">
|
||||
{t('app.admin.plan_form.ACTION_plan', { ACTION: action })}
|
||||
</FabButton>
|
||||
id="plan_category_id"
|
||||
tooltip={t('app.admin.plan_form.category_help')}
|
||||
label={t('app.admin.plan_form.category')} />}
|
||||
<FormInput register={register}
|
||||
formState={formState}
|
||||
id="ui_weight"
|
||||
type="number"
|
||||
label={t('app.admin.plan_form.visual_prominence')}
|
||||
tooltip={t('app.admin.plan_form.visual_prominence_help')} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<AdvancedAccountingForm register={register} onError={onError} />
|
||||
</section>
|
||||
|
||||
{action === 'update' && <PlanPricingForm formState={formState}
|
||||
control={control}
|
||||
onError={onError}
|
||||
setValue={setValue}
|
||||
register={register} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="plan-form">
|
||||
<header>
|
||||
<h2>{t('app.admin.plan_form.ACTION_title', { ACTION: action })}</h2>
|
||||
<div className="grpBtn">
|
||||
<FabButton type="submit" onClick={handleSubmit(onSubmit)} className="fab-button is-main">
|
||||
{t('app.admin.plan_form.save')}
|
||||
</FabButton>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<UnsavedFormAlert uiRouter={uiRouter} formState={formState} />
|
||||
<FabTabs tabs={[
|
||||
{
|
||||
id: 'settings',
|
||||
title: t('app.admin.plan_form.tab_settings'),
|
||||
content: renderSettingsTab()
|
||||
},
|
||||
{
|
||||
id: 'usageLimits',
|
||||
title: t('app.admin.plan_form.tab_usage_limits'),
|
||||
content: <PlanLimitForm control={control}
|
||||
register={register}
|
||||
formState={formState}
|
||||
onError={onError}
|
||||
getValues={getValues}
|
||||
resetField={resetField} />
|
||||
}
|
||||
]} />
|
||||
</form>
|
||||
|
||||
<PartnerModal isOpen={isOpenPartnerModal}
|
||||
toggleModal={tooglePartnerModal}
|
||||
onError={onError}
|
||||
@ -281,4 +379,4 @@ const PlanFormWrapper: React.FC<PlanFormProps> = (props) => {
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
Application.Components.component('planForm', react2angular(PlanFormWrapper, ['action', 'plan', 'onError', 'onSuccess']));
|
||||
Application.Components.component('planForm', react2angular(PlanFormWrapper, ['action', 'plan', 'onError', 'onSuccess', 'uiRouter']));
|
||||
|
261
app/frontend/src/javascript/components/plans/plan-limit-form.tsx
Normal file
261
app/frontend/src/javascript/components/plans/plan-limit-form.tsx
Normal file
@ -0,0 +1,261 @@
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import { Control, FormState, UseFormGetValues, UseFormResetField } from 'react-hook-form/dist/types/form';
|
||||
import { FormSwitch } from '../form/form-switch';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { PlanLimitModal } from './plan-limit-modal';
|
||||
import { Plan, PlanLimitation } from '../../models/plan';
|
||||
import { useFieldArray, UseFormRegister, useWatch } from 'react-hook-form';
|
||||
import { Machine } from '../../models/machine';
|
||||
import { MachineCategory } from '../../models/machine-category';
|
||||
import MachineAPI from '../../api/machine';
|
||||
import MachineCategoryAPI from '../../api/machine-category';
|
||||
import { FormUnsavedList } from '../form/form-unsaved-list';
|
||||
import { EditDestroyButtons } from '../base/edit-destroy-buttons';
|
||||
import { X } from 'phosphor-react';
|
||||
|
||||
interface PlanLimitFormProps<TContext extends object> {
|
||||
register: UseFormRegister<Plan>,
|
||||
control: Control<Plan, TContext>,
|
||||
formState: FormState<Plan>,
|
||||
onError: (message: string) => void,
|
||||
getValues: UseFormGetValues<Plan>,
|
||||
resetField: UseFormResetField<Plan>
|
||||
}
|
||||
|
||||
/**
|
||||
* Form tab to manage a subscription's usage limit
|
||||
*/
|
||||
export const PlanLimitForm = <TContext extends object> ({ register, control, formState, onError, getValues, resetField }: PlanLimitFormProps<TContext>) => {
|
||||
const { t } = useTranslation('admin');
|
||||
const { fields, append, remove, update } = useFieldArray<Plan, 'plan_limitations_attributes'>({ control, name: 'plan_limitations_attributes' });
|
||||
const limiting = useWatch<Plan>({ control, name: 'limiting' });
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [machines, setMachines] = useState<Array<Machine>>([]);
|
||||
const [categories, setCategories] = useState<Array<MachineCategory>>([]);
|
||||
const [edited, setEdited] = useState<{index: number, limitation: PlanLimitation}>(null);
|
||||
|
||||
useEffect(() => {
|
||||
MachineAPI.index({ disabled: false })
|
||||
.then(setMachines)
|
||||
.catch(onError);
|
||||
MachineCategoryAPI.index()
|
||||
.then(setCategories)
|
||||
.catch(onError);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Opens/closes the product stock edition modal
|
||||
*/
|
||||
const toggleModal = (): void => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on 'add a limitation'
|
||||
*/
|
||||
const onAddLimitation = (): void => {
|
||||
setEdited(null);
|
||||
toggleModal();
|
||||
};
|
||||
/**
|
||||
* Triggered when a new limit was added or an existing limit was modified
|
||||
*/
|
||||
const onLimitationSuccess = (limitation: PlanLimitation): void => {
|
||||
const id = getValues(`plan_limitations_attributes.${edited?.index}.id`);
|
||||
if (id) {
|
||||
update(edited.index, { ...limitation, id });
|
||||
setEdited(null);
|
||||
} else {
|
||||
append({ ...limitation, id });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when an unsaved limit was removed from the "pending" list.
|
||||
*/
|
||||
const onRemoveUnsaved = (index: number): void => {
|
||||
const id = getValues(`plan_limitations_attributes.${index}.id`);
|
||||
if (id) {
|
||||
// will reset the field to its default values
|
||||
resetField(`plan_limitations_attributes.${index}`);
|
||||
// unmount and remount the field
|
||||
update(index, getValues(`plan_limitations_attributes.${index}`));
|
||||
} else {
|
||||
remove(index);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when a saved limitation is requested to be deleted
|
||||
*/
|
||||
const handleLimitationDelete = (index: number): () => Promise<void> => {
|
||||
return () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
update(index, { ...getValues(`plan_limitations_attributes.${index}`), _destroy: true });
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on "cancel" for a limitated previsouly marked as deleted
|
||||
*/
|
||||
const cancelDeletion = (index: number): (event: React.MouseEvent<HTMLParagraphElement, MouseEvent>) => void => {
|
||||
return (event) => {
|
||||
event.preventDefault();
|
||||
update(index, { ...getValues(`plan_limitations_attributes.${index}`), _destroy: false });
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user wants to modify a limitation. Return a callback
|
||||
*/
|
||||
const onEditLimitation = (limitation: PlanLimitation, index: number): () => void => {
|
||||
return () => {
|
||||
setEdited({ index, limitation });
|
||||
toggleModal();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Render an unsaved limitation of use
|
||||
*/
|
||||
const renderOngoingLimit = (limit: PlanLimitation): ReactNode => (
|
||||
<>
|
||||
{(limit.limitable_type === 'MachineCategory' && <div className="group">
|
||||
<span>{t('app.admin.plan_limit_form.category')}</span>
|
||||
<p>{categories?.find(c => c.id === limit.limitable_id)?.name}</p>
|
||||
</div>) ||
|
||||
<div className="group">
|
||||
<span>{t('app.admin.plan_limit_form.machine')}</span>
|
||||
<p>{machines?.find(m => m.id === limit.limitable_id)?.name}</p>
|
||||
</div>}
|
||||
<div className="group">
|
||||
<span>{t('app.admin.plan_limit_form.max_hours_per_day')}</span>
|
||||
<p>{limit.limit}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="plan-limit-form">
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_limit_form.usage_limitation')}</p>
|
||||
<p className="description">{t('app.admin.plan_limit_form.usage_limitation_info')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
defaultValue={false}
|
||||
label={t('app.admin.plan_limit_form.usage_limitation_switch')}
|
||||
id="limiting" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{limiting && <div className="plan-limit-grp">
|
||||
<header>
|
||||
<p>{t('app.admin.plan_limit_form.all_limitations')}</p>
|
||||
<div className="grpBtn">
|
||||
<FabButton onClick={onAddLimitation} className="is-main">
|
||||
{t('app.admin.plan_limit_form.new_usage_limitation')}
|
||||
</FabButton>
|
||||
</div>
|
||||
</header>
|
||||
<FormUnsavedList fields={fields}
|
||||
onRemove={onRemoveUnsaved}
|
||||
register={register}
|
||||
title={t('app.admin.plan_limit_form.ongoing_limitations')}
|
||||
shouldRenderField={(limit: PlanLimitation) => limit._modified}
|
||||
formAttributeName="plan_limitations_attributes"
|
||||
formAttributes={['id', 'limitable_type', 'limitable_id', 'limit']}
|
||||
renderField={renderOngoingLimit}
|
||||
cancelLabel={t('app.admin.plan_limit_form.cancel')} />
|
||||
|
||||
{fields.filter(f => f._modified).length > 0 &&
|
||||
<p className="title">{t('app.admin.plan_limit_form.saved_limitations')}</p>
|
||||
}
|
||||
|
||||
{fields.filter(f => f.limitable_type === 'MachineCategory' && !f._modified).length > 0 &&
|
||||
<div className='plan-limit-list'>
|
||||
<p className="title">{t('app.admin.plan_limit_form.by_category')}</p>
|
||||
{fields.map((limitation, index) => {
|
||||
if (limitation.limitable_type !== 'MachineCategory' || limitation._modified) return false;
|
||||
|
||||
return (
|
||||
<div className={`plan-limit-item ${limitation._destroy ? 'is-destroying' : ''}`} key={limitation.id}>
|
||||
<div className="grp">
|
||||
<div>
|
||||
<span>{t('app.admin.plan_limit_form.category')}</span>
|
||||
<p>{categories.find(c => c.id === limitation.limitable_id)?.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('app.admin.plan_limit_form.max_hours_per_day')}</span>
|
||||
<p>{limitation.limit}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='actions'>
|
||||
<EditDestroyButtons onError={onError}
|
||||
onEdit={onEditLimitation(limitation, index)}
|
||||
itemId={getValues(`plan_limitations_attributes.${index}.id`)}
|
||||
showDestroyConfirmation={false}
|
||||
destroy={handleLimitationDelete(index)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}).filter(Boolean)}
|
||||
</div>
|
||||
}
|
||||
|
||||
{fields.filter(f => f.limitable_type === 'Machine' && !f._modified).length > 0 &&
|
||||
<div className='plan-limit-list'>
|
||||
<p className="title">{t('app.admin.plan_limit_form.by_machine')}</p>
|
||||
{fields.map((limitation, index) => {
|
||||
if (limitation.limitable_type !== 'Machine' || limitation._modified) return false;
|
||||
|
||||
return (
|
||||
<div className={`plan-limit-item ${limitation._destroy ? 'is-destroying' : ''}`} key={limitation.id}>
|
||||
<div className="grp">
|
||||
<div>
|
||||
<span>{t('app.admin.plan_limit_form.machine')}</span>
|
||||
<p>{machines.find(m => m.id === limitation.limitable_id)?.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('app.admin.plan_limit_form.max_hours_per_day')}</span>
|
||||
<p>{limitation.limit}</p>
|
||||
</div>
|
||||
{limitation._destroy && <div className="marker">{t('app.admin.plan_limit_form.ongoing_deletion')}</div>}
|
||||
</div>
|
||||
|
||||
<div className='actions'>
|
||||
{(limitation._destroy &&
|
||||
<p className="cancel-action" onClick={cancelDeletion(index)}>
|
||||
{t('app.admin.plan_limit_form.cancel_deletion')}
|
||||
<X size={14} />
|
||||
</p>) ||
|
||||
<EditDestroyButtons onError={onError}
|
||||
onEdit={onEditLimitation(limitation, index)}
|
||||
itemId={getValues(`plan_limitations_attributes.${index}.id`)}
|
||||
showDestroyConfirmation={false}
|
||||
destroy={handleLimitationDelete(index)} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}).filter(Boolean)}
|
||||
</div>
|
||||
}
|
||||
</div>}
|
||||
|
||||
<PlanLimitModal isOpen={isOpen}
|
||||
machines={machines}
|
||||
categories={categories}
|
||||
toggleModal={toggleModal}
|
||||
onSuccess={onLimitationSuccess}
|
||||
limitation={edited?.limitation}
|
||||
existingLimitations={fields} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,124 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { FormSelect } from '../form/form-select';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { LimitableType, PlanLimitation } from '../../models/plan';
|
||||
import { Machine } from '../../models/machine';
|
||||
import { MachineCategory } from '../../models/machine-category';
|
||||
import { SelectOption } from '../../models/select';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface PlanLimitModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
onSuccess: (limit: PlanLimitation) => void,
|
||||
machines: Array<Machine>
|
||||
categories: Array<MachineCategory>,
|
||||
limitation?: PlanLimitation,
|
||||
existingLimitations: Array<PlanLimitation>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form to manage subscriptions limitations of use
|
||||
*/
|
||||
export const PlanLimitModal: React.FC<PlanLimitModalProps> = ({ isOpen, toggleModal, machines, categories, onSuccess, limitation, existingLimitations = [] }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const { register, control, formState, setValue, handleSubmit, reset } = useForm<PlanLimitation>({ defaultValues: limitation || { limitable_type: 'MachineCategory' } });
|
||||
const limitType = useWatch({ control, name: 'limitable_type' });
|
||||
|
||||
useEffect(() => {
|
||||
reset(limitation);
|
||||
}, [limitation]);
|
||||
|
||||
/**
|
||||
* Toggle the form between 'categories' and 'machine'
|
||||
*/
|
||||
const toggleLimitType = (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>, type: LimitableType) => {
|
||||
evt.preventDefault();
|
||||
setValue('limitable_type', type);
|
||||
setValue('limitable_id', null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user validates the new limit.
|
||||
* We do not use handleSubmit() directly to prevent the propagaion of the "submit" event to the parent form
|
||||
*/
|
||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
return handleSubmit((data: PlanLimitation) => {
|
||||
onSuccess({ ...data, _modified: true });
|
||||
reset({ limitable_type: 'MachineCategory', limitable_id: null, limit: null });
|
||||
toggleModal();
|
||||
})(event);
|
||||
};
|
||||
/**
|
||||
* Creates options to the react-select format
|
||||
*/
|
||||
const buildOptions = (): Array<SelectOption<number>> => {
|
||||
if (limitType === 'MachineCategory') {
|
||||
return categories
|
||||
.filter(c => limitation || !existingLimitations.filter(l => l.limitable_type === 'MachineCategory').map(l => l.limitable_id).includes(c.id))
|
||||
.map(cat => {
|
||||
return { value: cat.id, label: cat.name };
|
||||
});
|
||||
} else {
|
||||
return machines
|
||||
.filter(m => limitation || !existingLimitations.filter(l => l.limitable_type === 'Machine').map(l => l.limitable_id).includes(m.id))
|
||||
.map(machine => {
|
||||
return { value: machine.id, label: machine.name };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal title={t('app.admin.plan_limit_modal.title')}
|
||||
width={ModalSize.large}
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
onClose={() => reset({ limitable_type: 'MachineCategory' })}
|
||||
closeButton>
|
||||
<form className='plan-limit-modal' onSubmit={onSubmit}>
|
||||
<p className='subtitle'>{t('app.admin.plan_limit_modal.limit_reservations')}</p>
|
||||
<div className="grp">
|
||||
<button onClick={evt => toggleLimitType(evt, 'MachineCategory')}
|
||||
className={limitType === 'MachineCategory' ? 'is-active' : ''}
|
||||
disabled={!!limitation}>
|
||||
{t('app.admin.plan_limit_modal.by_category')}
|
||||
</button>
|
||||
<button onClick={evt => toggleLimitType(evt, 'Machine')}
|
||||
className={limitType === 'Machine' ? 'is-active' : ''}
|
||||
disabled={!!limitation}>
|
||||
{t('app.admin.plan_limit_modal.by_machine')}
|
||||
</button>
|
||||
</div>
|
||||
<FabAlert level='info'>{limitType === 'Machine' ? t('app.admin.plan_limit_modal.machine_info') : t('app.admin.plan_limit_modal.categories_info')}</FabAlert>
|
||||
<FormInput register={register} id="id" type="hidden" />
|
||||
<FormInput register={register} id="limitable_type" type="hidden" />
|
||||
<FormSelect options={buildOptions()}
|
||||
disabled={!!limitation}
|
||||
control={control}
|
||||
id="limitable_id"
|
||||
rules={{ required: true }}
|
||||
formState={formState}
|
||||
label={t('app.admin.plan_limit_modal.machine')} />
|
||||
<FormInput id="limit"
|
||||
type="number"
|
||||
register={register}
|
||||
rules={{ required: true, min: 1 }}
|
||||
nullable
|
||||
step={1}
|
||||
formState={formState}
|
||||
label={t('app.admin.plan_limit_modal.max_hours_per_day')} />
|
||||
<FabButton type="submit">{t('app.admin.plan_limit_modal.confirm')}</FabButton>
|
||||
</form>
|
||||
</FabModal>
|
||||
);
|
||||
};
|
@ -92,32 +92,37 @@ export const PlanPricingForm = <TContext extends object>({ register, control, fo
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="plan-pricing-form">
|
||||
<h4>{t('app.admin.plan_pricing_form.prices')}</h4>
|
||||
{plans && <FormSelect options={plans}
|
||||
label={t('app.admin.plan_pricing_form.copy_prices_from')}
|
||||
tooltip={t('app.admin.plan_pricing_form.copy_prices_from_help')}
|
||||
control={control}
|
||||
onChange={handleCopyPrices}
|
||||
id="parent_plan_id" />}
|
||||
{<FabTabs tabs={[
|
||||
machines && {
|
||||
id: 'machines',
|
||||
title: t('app.admin.plan_pricing_form.machines'),
|
||||
content: fields.map((price, index) => {
|
||||
if (price.priceable_type !== 'Machine') return false;
|
||||
return renderPriceElement(price, index);
|
||||
}).filter(Boolean)
|
||||
},
|
||||
spaces && {
|
||||
id: 'spaces',
|
||||
title: t('app.admin.plan_pricing_form.spaces'),
|
||||
content: fields.map((price, index) => {
|
||||
if (price.priceable_type !== 'Space') return false;
|
||||
return renderPriceElement(price, index);
|
||||
}).filter(Boolean)
|
||||
}
|
||||
]} />}
|
||||
</div>
|
||||
<section className="plan-pricing-form" data-testid="plan-pricing-form">
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_pricing_form.prices')}</p>
|
||||
<p className="description">{t('app.admin.plan_pricing_form.about_prices')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
{plans && <FormSelect options={plans}
|
||||
control={control}
|
||||
label={t('app.admin.plan_pricing_form.copy_prices_from')}
|
||||
tooltip={t('app.admin.plan_pricing_form.copy_prices_from_help')}
|
||||
onChange={handleCopyPrices}
|
||||
id="parent_plan_id" />}
|
||||
{<FabTabs tabs={[
|
||||
machines && {
|
||||
id: 'machines',
|
||||
title: t('app.admin.plan_pricing_form.machines'),
|
||||
content: fields.map((price, index) => {
|
||||
if (price.priceable_type !== 'Machine') return false;
|
||||
return renderPriceElement(price, index);
|
||||
}).filter(Boolean)
|
||||
},
|
||||
spaces && {
|
||||
id: 'spaces',
|
||||
title: t('app.admin.plan_pricing_form.spaces'),
|
||||
content: fields.map((price, index) => {
|
||||
if (price.priceable_type !== 'Space') return false;
|
||||
return renderPriceElement(price, index);
|
||||
}).filter(Boolean)
|
||||
}
|
||||
]} />}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -114,7 +114,7 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
|
||||
onEdit={() => handleRequestEdit(p)}
|
||||
itemId={p.id}
|
||||
itemType={t('app.admin.configure_packs_button.pack')}
|
||||
apiDestroy={PrepaidPackAPI.destroy}/>
|
||||
destroy={PrepaidPackAPI.destroy}/>
|
||||
<FabModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
title={t('app.admin.configure_packs_button.edit_pack')}
|
||||
|
@ -1,22 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { Product } from '../../models/product';
|
||||
import { PencilSimple, Trash } from 'phosphor-react';
|
||||
import noImage from '../../../../images/no_image.png';
|
||||
import { FabStateLabel } from '../base/fab-state-label';
|
||||
import { ProductPrice } from './product-price';
|
||||
import { EditDestroyButtons } from '../base/edit-destroy-buttons';
|
||||
import ProductAPI from '../../api/product';
|
||||
|
||||
interface ProductItemProps {
|
||||
product: Product,
|
||||
onEdit: (product: Product) => void,
|
||||
onDelete: (productId: number) => void,
|
||||
onDelete: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a product item in the admin view
|
||||
*/
|
||||
export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDelete }) => {
|
||||
export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDelete, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
/**
|
||||
@ -34,15 +35,6 @@ export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDel
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Init the process of delete the given product
|
||||
*/
|
||||
const deleteProduct = (productId: number): () => void => {
|
||||
return (): void => {
|
||||
onDelete(productId);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns CSS class from stock status
|
||||
*/
|
||||
@ -80,14 +72,13 @@ export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDel
|
||||
<ProductPrice product={product} className="price" />
|
||||
</div>
|
||||
<div className='actions'>
|
||||
<div className='manage'>
|
||||
<FabButton className='edit-btn' onClick={editProduct(product)}>
|
||||
<PencilSimple size={20} weight="fill" />
|
||||
</FabButton>
|
||||
<FabButton className='delete-btn' onClick={deleteProduct(product.id)}>
|
||||
<Trash size={20} weight="fill" />
|
||||
</FabButton>
|
||||
</div>
|
||||
<EditDestroyButtons onDeleteSuccess={onDelete}
|
||||
className="manage"
|
||||
onError={onError}
|
||||
onEdit={editProduct(product)}
|
||||
itemId={product.id}
|
||||
itemType={t('app.admin.store.product_item.product')}
|
||||
destroy={ProductAPI.destroy} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import Select from 'react-select';
|
||||
import { PencilSimple, X } from 'phosphor-react';
|
||||
import { PencilSimple } from 'phosphor-react';
|
||||
import { useFieldArray, UseFormRegister } from 'react-hook-form';
|
||||
import { Control, FormState, UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Product,
|
||||
Product, ProductStockMovement,
|
||||
stockMovementAllReasons, StockMovementIndex, StockMovementIndexFilter,
|
||||
StockMovementReason,
|
||||
StockType
|
||||
@ -20,6 +20,7 @@ import FormatLib from '../../lib/format';
|
||||
import ProductLib from '../../lib/product';
|
||||
import { useImmer } from 'use-immer';
|
||||
import { FabPagination } from '../base/fab-pagination';
|
||||
import { FormUnsavedList } from '../form/form-unsaved-list';
|
||||
|
||||
interface ProductStockFormProps<TContext extends object> {
|
||||
currentFormValues: Product,
|
||||
@ -159,6 +160,25 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Render an attribute of an unsaved stock movement
|
||||
*/
|
||||
const renderOngoingStockMovement = (movement: ProductStockMovement): ReactNode => (
|
||||
<>
|
||||
<div className="group">
|
||||
<p>{t(`app.admin.store.product_stock_form.type_${ProductLib.stockMovementType(movement.reason)}`)}</p>
|
||||
</div>
|
||||
<div className="group">
|
||||
<span>{t(`app.admin.store.product_stock_form.${movement.stock_type}`)}</span>
|
||||
<p>{ProductLib.absoluteStockMovement(movement.quantity, movement.reason)}</p>
|
||||
</div>
|
||||
<div className="group">
|
||||
<span>{t('app.admin.store.product_stock_form.reason')}</span>
|
||||
<p>{t(ProductLib.stockMovementReasonTrKey(movement.reason))}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='product-stock-form'>
|
||||
<h4>{t('app.admin.store.product_stock_form.stock_up_to_date')}
|
||||
@ -178,36 +198,19 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
|
||||
<span>{t('app.admin.store.product_stock_form.external')}</span>
|
||||
<p>{currentFormValues?.stock?.external}</p>
|
||||
</div>
|
||||
<FabButton onClick={toggleModal} icon={<PencilSimple size={20} weight="fill" />} className="is-black">Modifier</FabButton>
|
||||
<FabButton onClick={toggleModal} icon={<PencilSimple size={20} weight="fill" />} className="is-black">{t('app.admin.store.product_stock_form.edit')}</FabButton>
|
||||
</div>
|
||||
|
||||
{fields.length > 0 && <div className="ongoing-stocks">
|
||||
<span className="title">{t('app.admin.store.product_stock_form.ongoing_operations')}</span>
|
||||
<span className="save-notice">{t('app.admin.store.product_stock_form.save_reminder')}</span>
|
||||
{fields.map((newMovement, index) => (
|
||||
<div key={index} className="unsaved-stock-movement stock-item">
|
||||
<div className="group">
|
||||
<p>{t(`app.admin.store.product_stock_form.type_${ProductLib.stockMovementType(newMovement.reason)}`)}</p>
|
||||
</div>
|
||||
<div className="group">
|
||||
<span>{t(`app.admin.store.product_stock_form.${newMovement.stock_type}`)}</span>
|
||||
<p>{ProductLib.absoluteStockMovement(newMovement.quantity, newMovement.reason)}</p>
|
||||
</div>
|
||||
<div className="group">
|
||||
<span>{t('app.admin.store.product_stock_form.reason')}</span>
|
||||
<p>{t(ProductLib.stockMovementReasonTrKey(newMovement.reason))}</p>
|
||||
</div>
|
||||
<p className="cancel-action" onClick={() => remove(index)}>
|
||||
{t('app.admin.store.product_stock_form.cancel')}
|
||||
<X size={20} />
|
||||
</p>
|
||||
<FormInput id={`product_stock_movements_attributes.${index}.stock_type`} register={register}
|
||||
type="hidden" />
|
||||
<FormInput id={`product_stock_movements_attributes.${index}.quantity`} register={register} type="hidden" />
|
||||
<FormInput id={`product_stock_movements_attributes.${index}.reason`} register={register} type="hidden" />
|
||||
</div>
|
||||
))}
|
||||
</div>}
|
||||
<FormUnsavedList fields={fields}
|
||||
className="ongoing-stocks"
|
||||
onRemove={remove}
|
||||
register={register}
|
||||
title={t('app.admin.store.product_stock_form.ongoing_operations')}
|
||||
formAttributeName="product_stock_movements_attributes"
|
||||
formAttributes={['stock_type', 'quantity', 'reason']}
|
||||
renderField={renderOngoingStockMovement}
|
||||
saveReminderLabel={t('app.admin.store.product_stock_form.save_reminder')}
|
||||
cancelLabel={t('app.admin.store.product_stock_form.cancel')} />
|
||||
|
||||
<hr />
|
||||
|
||||
|
@ -111,14 +111,9 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError, uiRouter }) =>
|
||||
};
|
||||
|
||||
/** Delete a product */
|
||||
const deleteProduct = async (productId: number): Promise<void> => {
|
||||
try {
|
||||
await ProductAPI.destroy(productId);
|
||||
await fetchProducts();
|
||||
onSuccess(t('app.admin.store.products.successfully_deleted'));
|
||||
} catch (e) {
|
||||
onError(t('app.admin.store.products.unable_to_delete') + e);
|
||||
}
|
||||
const deleteProduct = async (message: string): Promise<void> => {
|
||||
await fetchProducts();
|
||||
onSuccess(message);
|
||||
};
|
||||
|
||||
/** Goto new product page */
|
||||
@ -244,6 +239,7 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError, uiRouter }) =>
|
||||
<ProductItem
|
||||
key={product.id}
|
||||
product={product}
|
||||
onError={onError}
|
||||
onEdit={editProduct}
|
||||
onDelete={deleteProduct}
|
||||
/>
|
||||
|
@ -199,7 +199,7 @@ export const Trainings: React.FC<TrainingsProps> = ({ onError, onSuccess }) => {
|
||||
onEdit={() => toTrainingEdit(training)}
|
||||
itemId={training.id}
|
||||
itemType={t('app.admin.trainings.training')}
|
||||
apiDestroy={TrainingAPI.destroy}/>
|
||||
destroy={TrainingAPI.destroy}/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
@ -21,11 +21,14 @@
|
||||
/**
|
||||
* Controller used in the plan creation form
|
||||
*/
|
||||
Application.Controllers.controller('NewPlanController', ['$scope', '$uibModal', 'groups', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t', 'planCategories',
|
||||
function ($scope, $uibModal, groups, prices, partners, CSRF, $state, growl, _t, planCategories) {
|
||||
Application.Controllers.controller('NewPlanController', ['$scope', '$uibModal', 'groups', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t', '$uiRouter',
|
||||
function ($scope, $uibModal, groups, prices, partners, CSRF, $state, growl, _t, $uiRouter) {
|
||||
// protection against request forgery
|
||||
CSRF.setMetaTags();
|
||||
|
||||
// the following item is used by the UnsavedFormAlert component to detect a page change
|
||||
$scope.uiRouter = $uiRouter;
|
||||
|
||||
/**
|
||||
* Shows an error message forwarded from a child component
|
||||
*/
|
||||
@ -46,13 +49,16 @@ Application.Controllers.controller('NewPlanController', ['$scope', '$uibModal',
|
||||
/**
|
||||
* Controller used in the plan edition form
|
||||
*/
|
||||
Application.Controllers.controller('EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'spaces', 'prices', 'partners', 'CSRF', '$state', '$transition$', 'growl', '$filter', '_t', 'Plan', 'planCategories',
|
||||
function ($scope, groups, plans, planPromise, machines, spaces, prices, partners, CSRF, $state, $transition$, growl, $filter, _t, Plan, planCategories) {
|
||||
Application.Controllers.controller('EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'spaces', 'prices', 'partners', 'CSRF', '$state', '$transition$', 'growl', '$filter', '_t', '$uiRouter',
|
||||
function ($scope, groups, plans, planPromise, machines, spaces, prices, partners, CSRF, $state, $transition$, growl, $filter, _t, $uiRouter) {
|
||||
// protection against request forgery
|
||||
CSRF.setMetaTags();
|
||||
|
||||
$scope.suscriptionPlan = cleanPlan(planPromise);
|
||||
|
||||
// the following item is used by the UnsavedFormAlert component to detect a page change
|
||||
$scope.uiRouter = $uiRouter;
|
||||
|
||||
/**
|
||||
* Shows an error message forwarded from a child component
|
||||
*/
|
||||
|
@ -12,6 +12,16 @@ export interface Partner {
|
||||
email: string
|
||||
}
|
||||
|
||||
export type LimitableType = 'Machine'|'MachineCategory';
|
||||
export interface PlanLimitation {
|
||||
id?: number,
|
||||
limitable_id: number,
|
||||
limitable_type: LimitableType,
|
||||
limit: number,
|
||||
_modified?: boolean,
|
||||
_destroy?: boolean,
|
||||
}
|
||||
|
||||
export interface Plan {
|
||||
id?: number,
|
||||
base_name: string,
|
||||
@ -34,8 +44,10 @@ export interface Plan {
|
||||
plan_file_url?: string,
|
||||
partner_id?: number,
|
||||
partnership?: boolean,
|
||||
limiting?: boolean,
|
||||
partners?: Array<Partner>,
|
||||
advanced_accounting_attributes?: AdvancedAccounting
|
||||
advanced_accounting_attributes?: AdvancedAccounting,
|
||||
plan_limitations_attributes?: Array<PlanLimitation>
|
||||
}
|
||||
|
||||
export interface PlansDuration {
|
||||
|
@ -62,6 +62,7 @@
|
||||
@import "modules/form/form-checklist";
|
||||
@import "modules/form/form-file-upload";
|
||||
@import "modules/form/form-image-upload";
|
||||
@import "modules/form/form-unsaved-list";
|
||||
@import "modules/group/change-group";
|
||||
@import "modules/invoices/invoices-settings-panel";
|
||||
@import "modules/invoices/vat-settings-modal";
|
||||
@ -97,6 +98,9 @@
|
||||
@import "modules/plan-categories/plan-categories-list";
|
||||
@import "modules/plans/plan-card";
|
||||
@import "modules/plans/plan-form";
|
||||
@import "modules/plans/plan-limit-form";
|
||||
@import "modules/plans/plan-limit-modal";
|
||||
@import "modules/plans/plan-pricing-form";
|
||||
@import "modules/plans/plans-filter";
|
||||
@import "modules/plans/plans-list";
|
||||
@import "modules/prepaid-packs/packs-summary";
|
||||
|
@ -12,16 +12,21 @@
|
||||
text-align: center;
|
||||
color: var(--main);
|
||||
border-bottom: 1px solid var(--gray-soft-dark);
|
||||
border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0;
|
||||
&:hover {
|
||||
background-color: var(--gray-soft-light);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.react-tabs__tab--selected {
|
||||
color: var(--gray-hard-dark);
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
border-bottom: none;
|
||||
border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--gray-soft-lightest);
|
||||
cursor: default;
|
||||
}
|
||||
&:focus { outline: none; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
.fab-text-editor {
|
||||
position: relative;
|
||||
margin-bottom: 1.6rem;
|
||||
//margin-bottom: 1.6rem;
|
||||
padding-bottom: 1.6rem;
|
||||
background-color: var(--gray-soft-lightest);
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
|
@ -15,8 +15,6 @@
|
||||
flex-direction: column;
|
||||
gap: 3.2rem;
|
||||
|
||||
.fab-alert { margin: 0; }
|
||||
|
||||
section { @include layout-settings; }
|
||||
.save-btn { align-self: flex-start; }
|
||||
}
|
||||
@ -25,7 +23,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2.4rem;
|
||||
gap: 1.6rem 2.4rem;
|
||||
|
||||
.add-price {
|
||||
max-width: fit-content;
|
||||
@ -37,6 +35,8 @@
|
||||
align-items: flex-end;
|
||||
gap: 0 2.4rem;
|
||||
|
||||
.form-item { margin: 0; }
|
||||
|
||||
.remove-price {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@ -61,8 +61,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 640px) {flex-direction: row; }
|
||||
|
||||
.form-item:first-child {
|
||||
margin-right: 2.4rem;
|
||||
}
|
||||
.form-item { margin: 0; }
|
||||
.form-item:first-child { margin-right: 2.4rem; }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,47 @@
|
||||
.form-unsaved-list {
|
||||
.save-notice {
|
||||
@include text-xs;
|
||||
margin-left: 1rem;
|
||||
color: var(--alert);
|
||||
}
|
||||
.unsaved-field {
|
||||
background-color: var(--gray-soft-light);
|
||||
border: 0;
|
||||
padding: 1.2rem;
|
||||
margin-top: 1rem;width: 100%;
|
||||
display: flex;
|
||||
gap: 4.8rem;
|
||||
justify-items: flex-start;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
& > * { flex: 1 1 45%; }
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
@include text-base;
|
||||
}
|
||||
.title {
|
||||
@include text-base(600);
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
.group {
|
||||
span {
|
||||
@include text-xs;
|
||||
color: var(--gray-hard-light);
|
||||
}
|
||||
p { @include text-base(600); }
|
||||
}
|
||||
|
||||
.cancel-action {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
svg {
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,8 +15,6 @@
|
||||
flex-direction: column;
|
||||
gap: 3.2rem;
|
||||
|
||||
.fab-alert { margin: 0; }
|
||||
|
||||
section { @include layout-settings; }
|
||||
.save-btn { align-self: flex-start; }
|
||||
}
|
||||
|
@ -1,25 +1,40 @@
|
||||
.plan-form {
|
||||
.plan-sheet {
|
||||
margin-top: 4rem;
|
||||
max-width: 1260px;
|
||||
margin: 2.4rem auto 0;
|
||||
padding: 0 3rem 6rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
& > header {
|
||||
padding-bottom: 0;
|
||||
@include header($sticky: true);
|
||||
gap: 2.4rem;
|
||||
}
|
||||
.duration {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.form-item:first-child {
|
||||
margin-right: 32px;
|
||||
}
|
||||
}
|
||||
.partner {
|
||||
&-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 3.2rem;
|
||||
|
||||
.fab-alert {
|
||||
width: 100%;
|
||||
section { @include layout-settings; }
|
||||
.grp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media (min-width: 640px) {flex-direction: row; }
|
||||
|
||||
.form-item { margin: 0; }
|
||||
.form-item:first-child { margin-right: 2.4rem; }
|
||||
}
|
||||
|
||||
.partner {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: flex-end;
|
||||
gap: 0 2.4rem;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
flex-direction: row;
|
||||
button { margin-bottom: 1.6rem; }
|
||||
}
|
||||
}
|
||||
}
|
||||
.submit-btn {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,85 @@
|
||||
.plan-limit-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3.2rem 0;
|
||||
|
||||
section { @include layout-settings; }
|
||||
|
||||
.plan-limit-grp {
|
||||
header {
|
||||
@include header();
|
||||
p {
|
||||
@include title-base;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.form-unsaved-list {
|
||||
margin-bottom: 6.4rem;
|
||||
}
|
||||
.plan-limit-list {
|
||||
max-height: 65vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
& > .title { @include text-base(500); }
|
||||
.plan-limit-item {
|
||||
width: 100%;
|
||||
margin-bottom: 2.4rem;
|
||||
padding: 1.6rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 3.2rem;
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--gray-soft-lightest);
|
||||
|
||||
span {
|
||||
@include text-xs;
|
||||
color: var(--gray-hard-light);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
@include text-base(600);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 540px) {
|
||||
.grp {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
& > * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-destroying {
|
||||
background-color: var(--alert-lightest);
|
||||
.marker {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
color: var(--alert-dark);
|
||||
margin: auto;
|
||||
}
|
||||
.actions > .cancel-action {
|
||||
font-weight: normal;
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
.plan-limit-modal {
|
||||
.grp {
|
||||
margin-bottom: 3.2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 1.6rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--gray-soft-lightest);
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
color: var(--gray-soft-darkest);
|
||||
@include text-base;
|
||||
&.is-active {
|
||||
border: 1px solid var(--gray-soft-darkest);
|
||||
background-color: var(--gray-hard-darkest);
|
||||
color: var(--gray-soft-lightest);
|
||||
}
|
||||
}
|
||||
button:first-of-type {
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||
}
|
||||
button:last-of-type {
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
.plan-pricing-form {
|
||||
.fab-tabs .tabs li {
|
||||
margin-bottom: 1.6rem;
|
||||
&:hover { background-color: var(--gray-soft); }
|
||||
&.react-tabs__tab--selected:hover { background-color: transparent; }
|
||||
}
|
||||
|
||||
.react-tabs__tab-panel {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
@ -15,8 +15,6 @@
|
||||
flex-direction: column;
|
||||
gap: 3.2rem;
|
||||
|
||||
.fab-alert { margin: 0; }
|
||||
|
||||
section { @include layout-settings; }
|
||||
.save-btn { align-self: flex-start; }
|
||||
}
|
||||
|
@ -3,28 +3,6 @@
|
||||
|
||||
.ongoing-stocks {
|
||||
margin: 2.4rem 0;
|
||||
.save-notice {
|
||||
@include text-xs;
|
||||
margin-left: 1rem;
|
||||
color: var(--alert);
|
||||
}
|
||||
.unsaved-stock-movement {
|
||||
background-color: var(--gray-soft-light);
|
||||
border: 0;
|
||||
padding: 1.2rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
.cancel-action {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
svg {
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.store-list {
|
||||
margin-top: 2.4rem;
|
||||
|
@ -15,8 +15,6 @@
|
||||
flex-direction: column;
|
||||
gap: 3.2rem;
|
||||
|
||||
.fab-alert { margin: 0; }
|
||||
|
||||
section { @include layout-settings; }
|
||||
.save-btn { align-self: flex-start; }
|
||||
}
|
||||
|
@ -29,10 +29,15 @@
|
||||
}
|
||||
|
||||
& > .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.6rem;
|
||||
background-color: var(--gray-soft-light);
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
border-radius: var(--border-radius);
|
||||
& > * { margin-bottom: 0; }
|
||||
& > *:not(:last-child) { margin-bottom: 3.2rem; }
|
||||
.fab-alert { margin: 0 0 1.6rem; }
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
|
@ -10,24 +10,7 @@
|
||||
<h1>{{ 'app.admin.plans.edit.subscription_plan' | translate }} {{ suscriptionPlan.base_name }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<section class="heading-actions wrapper">
|
||||
<a class="btn btn-lg btn-block btn-default m-t-xs" ui-sref="app.admin.pricing" translate>{{ 'app.shared.buttons.cancel' }}</a>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="row no-gutter">
|
||||
<div class=" col-sm-12 col-md-9 b-r nopadding">
|
||||
|
||||
<div class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
<plan-form action="'update'" plan="suscriptionPlan" on-error="onError" on-success="onSuccess"></plan-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<plan-form action="'update'" plan="suscriptionPlan" on-error="onError" on-success="onSuccess" ui-router="uiRouter"></plan-form>
|
||||
|
@ -14,14 +14,4 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="row no-gutter">
|
||||
<div class=" col-sm-12 col-md-9 b-r nopadding">
|
||||
|
||||
<div class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
<plan-form action="'create'" on-error="onError" on-success="onSuccess"></plan-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<plan-form action="'create'" on-error="onError" on-success="onSuccess" ui-router="uiRouter"></plan-form>
|
||||
|
@ -13,18 +13,22 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [Plan,NilClass]
|
||||
def plan
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [User]
|
||||
def operator
|
||||
operator_profile.user
|
||||
end
|
||||
|
||||
# @return [User]
|
||||
def customer
|
||||
customer_profile.user
|
||||
end
|
||||
|
||||
# @return [Hash{Symbol=>Integer,Hash{Symbol=>Array<Hash{Symbol=>Integer,Float,Boolean,Time}>}}]
|
||||
def price
|
||||
is_privileged = operator.privileged? && operator.id != customer.id
|
||||
prepaid = { minutes: PrepaidPackService.minutes_available(customer, reservable) }
|
||||
@ -48,49 +52,35 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
{ elements: elements, amount: amount }
|
||||
end
|
||||
|
||||
# @return [String,NilClass]
|
||||
def name
|
||||
reservable&.name
|
||||
end
|
||||
|
||||
# @param all_items [Array<CartItem::BaseItem>]
|
||||
# @return [Boolean]
|
||||
def valid?(all_items = [])
|
||||
pending_subscription = all_items.find { |i| i.is_a?(CartItem::Subscription) }
|
||||
plan = pending_subscription&.plan || customer&.subscribed_plan
|
||||
|
||||
reservation_deadline_minutes = Setting.get('reservation_deadline').to_i
|
||||
reservation_deadline = reservation_deadline_minutes.minutes.since
|
||||
|
||||
unless ReservationLimitService.authorized?(plan, customer, self, all_items)
|
||||
errors.add(:reservation, I18n.t('cart_item_validation.limit_reached', {
|
||||
HOURS: ReservationLimitService.limit(plan, reservable),
|
||||
RESERVABLE: reservable.name
|
||||
}))
|
||||
return false
|
||||
end
|
||||
|
||||
cart_item_reservation_slots.each do |sr|
|
||||
slot = sr.slot
|
||||
if slot.nil?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.slot'))
|
||||
return false
|
||||
end
|
||||
|
||||
availability = slot.availability
|
||||
if availability.nil?
|
||||
errors.add(:availability, I18n.t('cart_item_validation.availability'))
|
||||
return false
|
||||
end
|
||||
|
||||
if slot.full?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.full'))
|
||||
return false
|
||||
end
|
||||
|
||||
if slot.start_at < reservation_deadline && !operator.privileged?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes }))
|
||||
return false
|
||||
end
|
||||
|
||||
next if availability.plan_ids.empty?
|
||||
next if required_subscription?(availability, pending_subscription)
|
||||
|
||||
errors.add(:availability, I18n.t('cart_item_validation.restricted'))
|
||||
return false
|
||||
return false unless validate_slot_reservation(sr, pending_subscription, reservation_deadline_minutes, errors)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# @return [Reservation]
|
||||
def to_object
|
||||
::Reservation.new(
|
||||
reservable_id: reservable_id,
|
||||
@ -107,7 +97,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
end
|
||||
|
||||
# Group the slots by date, if the extended_prices_in_same_day option is set to true
|
||||
# @return Hash{Symbol => Array<CartItem::ReservationSlot>}
|
||||
# @return [Hash{Symbol => Array<CartItem::ReservationSlot>}]
|
||||
def grouped_slots
|
||||
return { all: cart_item_reservation_slots } unless Setting.get('extended_prices_in_same_day')
|
||||
|
||||
@ -125,7 +115,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
# @option options [Boolean] :has_credits true if the user still has credits for the given slot, false if not provided
|
||||
# @option options [Boolean] :is_division false if the slot covers a full availability, true if it is a subdivision (default)
|
||||
# @option options [Number] :prepaid_minutes number of remaining prepaid minutes for the customer
|
||||
# @return [Float]
|
||||
# @return [Float,Integer]
|
||||
def get_slot_price_from_prices(prices, slot_reservation, is_privileged, options = {})
|
||||
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||
|
||||
@ -152,7 +142,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
# @option options [Boolean] :has_credits true if the user still has credits for the given slot, false if not provided
|
||||
# @option options [Boolean] :is_division false if the slot covers a full availability, true if it is a subdivision (default)
|
||||
# @option options [Number] :prepaid_minutes number of remaining prepaid minutes for the customer
|
||||
# @return [Float] price of the slot
|
||||
# @return [Float,Integer] price of the slot
|
||||
def get_slot_price(hourly_rate, slot_reservation, is_privileged, options = {})
|
||||
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||
|
||||
@ -245,14 +235,50 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
cart_item_reservation_slots.map { |sr| { id: sr.slots_reservation_id, slot_id: sr.slot_id, offered: sr.offered } }
|
||||
end
|
||||
|
||||
##
|
||||
# Check if the given availability requires a valid subscription. If so, check if the current customer
|
||||
# has the required susbcription, otherwise, check if the operator is privileged
|
||||
##
|
||||
# @param availability [Availability]
|
||||
# @param pending_subscription [CartItem::Subscription, NilClass]
|
||||
def required_subscription?(availability, pending_subscription)
|
||||
(customer.subscribed_plan && availability.plan_ids.include?(customer.subscribed_plan.id)) ||
|
||||
(pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) ||
|
||||
(operator.manager? && customer.id != operator.id) ||
|
||||
operator.admin?
|
||||
end
|
||||
|
||||
# @param reservation_slot [CartItem::ReservationSlot]
|
||||
# @param pending_subscription [CartItem::Subscription, NilClass]
|
||||
# @param reservation_deadline [Integer]
|
||||
# @param errors [ActiveModel::Errors]
|
||||
# @return [Boolean]
|
||||
def validate_slot_reservation(reservation_slot, pending_subscription, reservation_deadline, errors)
|
||||
slot = reservation_slot.slot
|
||||
if slot.nil?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.slot'))
|
||||
return false
|
||||
end
|
||||
|
||||
availability = slot.availability
|
||||
if availability.nil?
|
||||
errors.add(:availability, I18n.t('cart_item_validation.availability'))
|
||||
return false
|
||||
end
|
||||
|
||||
if slot.full?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.full'))
|
||||
return false
|
||||
end
|
||||
|
||||
if slot.start_at < reservation_deadline.minutes.since && !operator.privileged?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline }))
|
||||
return false
|
||||
end
|
||||
|
||||
if availability.plan_ids.any? && !required_subscription?(availability, pending_subscription)
|
||||
errors.add(:availability, I18n.t('cart_item_validation.restricted'))
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
@ -42,6 +42,8 @@ class Machine < ApplicationRecord
|
||||
|
||||
belongs_to :machine_category
|
||||
|
||||
has_many :plan_limitations, dependent: :destroy, inverse_of: :machine, foreign_type: 'limitable_type', foreign_key: 'limitable_id'
|
||||
|
||||
after_create :create_statistic_subtype
|
||||
after_create :create_machine_prices
|
||||
after_create :update_gateway_product
|
||||
|
@ -4,4 +4,5 @@
|
||||
class MachineCategory < ApplicationRecord
|
||||
has_many :machines, dependent: :nullify
|
||||
accepts_nested_attributes_for :machines, allow_destroy: true
|
||||
has_many :plan_limitations, dependent: :destroy, inverse_of: :machine_category, foreign_type: 'limitable_type', foreign_key: 'limitable_id'
|
||||
end
|
||||
|
@ -21,6 +21,9 @@ class Plan < ApplicationRecord
|
||||
has_many :cart_item_subscriptions, class_name: 'CartItem::Subscription', dependent: :destroy
|
||||
has_many :cart_item_payment_schedules, class_name: 'CartItem::PaymentSchedule', dependent: :destroy
|
||||
|
||||
has_many :plan_limitations, dependent: :destroy
|
||||
accepts_nested_attributes_for :plan_limitations, allow_destroy: true
|
||||
|
||||
extend FriendlyId
|
||||
friendly_id :base_name, use: :slugged
|
||||
|
||||
|
20
app/models/plan_limitation.rb
Normal file
20
app/models/plan_limitation.rb
Normal file
@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Allows to set booking limits on some resources, per plan.
|
||||
class PlanLimitation < ApplicationRecord
|
||||
belongs_to :plan
|
||||
|
||||
belongs_to :limitable, polymorphic: true
|
||||
belongs_to :machine, foreign_type: 'Machine', foreign_key: 'limitable_id', inverse_of: :plan_limitations
|
||||
belongs_to :machine_category, foreign_type: 'MachineCategory', foreign_key: 'limitable_id', inverse_of: :plan_limitations
|
||||
|
||||
validates :limitable_id, :limitable_type, :limit, :plan_id, presence: true
|
||||
validates :limitable_id, uniqueness: { scope: %i[limitable_type plan_id] }
|
||||
|
||||
# @return [Array<Machine,Event,Space,Training>]
|
||||
def reservables
|
||||
return limitable.machines if limitable_type == 'MachineCategory'
|
||||
|
||||
[limitable]
|
||||
end
|
||||
end
|
@ -32,6 +32,7 @@ class Reservation < ApplicationRecord
|
||||
after_commit :notify_member_create_reservation, on: :create
|
||||
after_commit :notify_admin_member_create_reservation, on: :create
|
||||
after_commit :extend_subscription, on: :create
|
||||
after_commit :notify_member_limitation_reached, on: :create
|
||||
|
||||
delegate :user, to: :statistic_profile
|
||||
|
||||
@ -137,4 +138,14 @@ class Reservation < ApplicationRecord
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
end
|
||||
|
||||
def notify_member_limitation_reached
|
||||
date = ReservationLimitService.reached_limit_date(self)
|
||||
return if date.nil?
|
||||
|
||||
NotificationCenter.call type: 'notify_member_reservation_limit_reached',
|
||||
receiver: user,
|
||||
attached_object: ReservationLimitService.limit(user.subscribed_plan, reservable),
|
||||
meta_data: { date: date }
|
||||
end
|
||||
end
|
||||
|
@ -160,6 +160,7 @@ class ShoppingCart
|
||||
|
||||
# Check if the current cart needs the user to have been validated, and if the condition is satisfied.
|
||||
# Return an array of errors, if any; false otherwise
|
||||
# @return [Array<String>,FalseClass]
|
||||
def check_user_validation(items)
|
||||
user_validation_required = Setting.get('user_validation_required')
|
||||
user_validation_required_list = Setting.get('user_validation_required_list')
|
||||
|
@ -125,27 +125,17 @@ class Availabilities::AvailabilitiesService
|
||||
# @param range_end [ActiveSupport::TimeWithZone]
|
||||
# @return ActiveRecord::Relation<Availability>
|
||||
def availabilities(availabilities, type, user, range_start, range_end)
|
||||
# who made the request?
|
||||
# 1) an admin (he can see all availabilities from 1 month ago to anytime in the future)
|
||||
if @current_user&.admin? || @current_user&.manager?
|
||||
window_start = [range_start, 1.month.ago].max
|
||||
availabilities.includes(:tags, :slots)
|
||||
.joins(:slots)
|
||||
.where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', range_end, window_start, type)
|
||||
.where('slots.start_at > ? AND slots.end_at < ?', window_start, range_end)
|
||||
# 2) an user (he cannot see past availabilities neither those further than 1 (or 3) months in the future)
|
||||
else
|
||||
end_at = @maximum_visibility[:other]
|
||||
end_at = @maximum_visibility[:year] if subscription_year?(user) && type != 'training'
|
||||
end_at = @maximum_visibility[:year] if show_more_trainings?(user) && type == 'training'
|
||||
window_end = [end_at, range_end].min
|
||||
window_start = [range_start, @minimum_visibility].max
|
||||
availabilities.includes(:tags, :slots)
|
||||
.joins(:slots)
|
||||
.where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', window_end, window_start, type)
|
||||
.where('slots.start_at > ? AND slots.end_at < ?', window_start, window_end)
|
||||
.where('availability_tags.tag_id' => user&.tag_ids&.concat([nil]))
|
||||
.where(lock: false)
|
||||
window = Availabilities::VisibilityService.new.visibility(@current_user, type, range_start, range_end)
|
||||
qry = availabilities.includes(:tags, :slots)
|
||||
.joins(:slots)
|
||||
.where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', window[1], window[0], type)
|
||||
.where('slots.start_at > ? AND slots.end_at < ?', window[0], window[1])
|
||||
unless @current_user&.privileged?
|
||||
# non priviledged users cannot see availabilities with tags different than their own and locked tags
|
||||
qry = qry.where('availability_tags.tag_id' => user&.tag_ids&.concat([nil]))
|
||||
.where(lock: false)
|
||||
end
|
||||
|
||||
qry
|
||||
end
|
||||
end
|
||||
|
60
app/services/availabilities/visibility_service.rb
Normal file
60
app/services/availabilities/visibility_service.rb
Normal file
@ -0,0 +1,60 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Return the maximum available visibility for a user
|
||||
class Availabilities::VisibilityService
|
||||
def initialize
|
||||
@maximum_visibility = {
|
||||
year: Setting.get('visibility_yearly').to_i.months.since,
|
||||
other: Setting.get('visibility_others').to_i.months.since
|
||||
}
|
||||
@minimum_visibility = Setting.get('reservation_deadline').to_i.minutes.since
|
||||
end
|
||||
|
||||
# @param user [User,NilClass]
|
||||
# @param available_type [String] 'training', 'space', 'machine' or 'event'
|
||||
# @param range_start [ActiveSupport::TimeWithZone]
|
||||
# @param range_end [ActiveSupport::TimeWithZone]
|
||||
# @return [Array<ActiveSupport::TimeWithZone,Date,Time>] as: [start,end]
|
||||
def visibility(user, available_type, range_start, range_end)
|
||||
if user&.privileged?
|
||||
window_start = [range_start, 1.month.ago].max
|
||||
window_end = range_end
|
||||
else
|
||||
end_at = @maximum_visibility[:other]
|
||||
end_at = @maximum_visibility[:year] if subscription_year?(user) && available_type != 'training'
|
||||
end_at = @maximum_visibility[:year] if show_more_trainings?(user) && available_type == 'training'
|
||||
end_at = subscription_visibility(user, available_type) || end_at
|
||||
window_end = [end_at, range_end].min
|
||||
window_start = [range_start, @minimum_visibility].max
|
||||
end
|
||||
[window_start, window_end]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param user [User,NilClass]
|
||||
def subscription_year?(user)
|
||||
user&.subscribed_plan &&
|
||||
(user&.subscribed_plan&.interval == 'year' ||
|
||||
(user&.subscribed_plan&.interval == 'month' && user.subscribed_plan.interval_count >= 12))
|
||||
end
|
||||
|
||||
# @param user [User,NilClass]
|
||||
# @param available_type [String] 'training', 'space', 'machine' or 'event'
|
||||
# @return [Time,NilClass]
|
||||
def subscription_visibility(user, available_type)
|
||||
return nil unless user&.subscribed_plan
|
||||
return nil unless available_type == 'machine'
|
||||
|
||||
machines = user&.subscribed_plan&.machines_visibility
|
||||
machines&.hours&.since
|
||||
end
|
||||
|
||||
# members must have validated at least 1 training and must have a valid yearly subscription to view
|
||||
# the trainings further in the futur. This is used to prevent users with a rolling subscription to take
|
||||
# their first training in a very long delay.
|
||||
# @param user [User,NilClass]
|
||||
def show_more_trainings?(user)
|
||||
user&.trainings&.size&.positive? && subscription_year?(user)
|
||||
end
|
||||
end
|
@ -6,10 +6,9 @@ class CartService
|
||||
@operator = operator
|
||||
end
|
||||
|
||||
##
|
||||
# For details about the expected hash format
|
||||
# @see app/frontend/src/javascript/models/payment.ts > interface ShoppingCart
|
||||
##
|
||||
# @return [ShoppingCart]
|
||||
def from_hash(cart_items)
|
||||
cart_items.permit! if cart_items.is_a? ActionController::Parameters
|
||||
|
||||
|
@ -73,7 +73,7 @@ class PrepaidPackService
|
||||
|
||||
# Total number of prepaid minutes available
|
||||
# @param user [User]
|
||||
# @param priceable [Machine,Space]
|
||||
# @param priceable [Machine,Space,NilClass]
|
||||
def minutes_available(user, priceable)
|
||||
return 0 if Setting.get('pack_only_for_subscription') && !user.subscribed_plan
|
||||
|
||||
|
97
app/services/reservation_limit_service.rb
Normal file
97
app/services/reservation_limit_service.rb
Normal file
@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Check if a user if allowed to book a reservation without exceeding the limits set by his plan
|
||||
class ReservationLimitService
|
||||
class << self
|
||||
# @param plan [Plan,NilClass]
|
||||
# @param customer [User]
|
||||
# @param reservation [CartItem::Reservation]
|
||||
# @param cart_items [Array<CartItem::BaseItem>]
|
||||
# @return [Boolean]
|
||||
def authorized?(plan, customer, reservation, cart_items)
|
||||
return true if plan.nil? || !plan.limiting
|
||||
|
||||
return true if reservation.nil? || !reservation.is_a?(CartItem::Reservation)
|
||||
|
||||
limit = limit(plan, reservation.reservable)
|
||||
return true if limit.nil?
|
||||
|
||||
reservation.cart_item_reservation_slots.group_by { |sr| sr.slot.start_at.to_date }.each_pair do |date, reservation_slots|
|
||||
daily_duration = reservations_duration(customer, date, reservation, cart_items) +
|
||||
(reservation_slots.map { |sr| sr.slot.duration }.reduce(:+) || 0)
|
||||
return false if Rational(daily_duration / 3600).to_f > limit.limit
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# @param reservation [Reservation]
|
||||
# @return [Date,NilClass]
|
||||
def reached_limit_date(reservation)
|
||||
user = reservation.user
|
||||
plan = user.subscribed_plan
|
||||
return nil if plan.nil? || !plan.limiting
|
||||
|
||||
limit = limit(plan, reservation.reservable)
|
||||
return nil if limit.nil?
|
||||
|
||||
reservation.slots_reservations.group_by { |sr| sr.slot.start_at.to_date }.each_pair do |date, reservation_slots|
|
||||
daily_duration = saved_reservations_durations(user, reservation.reservable, date, reservation) +
|
||||
(reservation_slots.map { |sr| sr.slot.duration }.reduce(:+) || 0)
|
||||
return date if Rational(daily_duration / 3600).to_f >= limit.limit
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
# @param plan [Plan,NilClass]
|
||||
# @param reservable [Machine,Event,Space,Training]
|
||||
# @return [PlanLimitation] in hours
|
||||
def limit(plan, reservable)
|
||||
return nil unless plan&.limiting
|
||||
|
||||
limitations = plan&.plan_limitations&.filter { |limit| limit.reservables.include?(reservable) }
|
||||
limitations&.find { |limit| limit.limitable_type != 'MachineCategory' } || limitations&.first
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param customer [User]
|
||||
# @param date [Date]
|
||||
# @param reservation [CartItem::Reservation]
|
||||
# @param cart_items [Array<CartItem::BaseItem>]
|
||||
# @return [Integer] in seconds
|
||||
def reservations_duration(customer, date, reservation, cart_items)
|
||||
daily_reservations_hours = saved_reservations_durations(customer, reservation.reservable, date)
|
||||
|
||||
cart_daily_reservations = cart_items.filter do |item|
|
||||
item.is_a?(CartItem::Reservation) &&
|
||||
item != reservation &&
|
||||
item.reservable == reservation.reservable &&
|
||||
item.cart_item_reservation_slots
|
||||
.includes(:slot)
|
||||
.where("date_trunc('day', slots.start_at) = :date", date: date)
|
||||
end
|
||||
|
||||
daily_reservations_hours +
|
||||
(cart_daily_reservations.map { |r| r.cart_item_reservation_slots.map { |sr| sr.slot.duration } }.flatten.reduce(:+) || 0)
|
||||
end
|
||||
|
||||
# @param customer [User]
|
||||
# @param reservable [Machine,Event,Space,Training]
|
||||
# @param date [Date]
|
||||
# @param reservation [Reservation]
|
||||
# @return [Integer] in seconds
|
||||
def saved_reservations_durations(customer, reservable, date, reservation = nil)
|
||||
daily_reservations = customer.reservations
|
||||
.includes(slots_reservations: :slot)
|
||||
.where(reservable: reservable)
|
||||
.where(slots_reservations: { canceled_at: nil })
|
||||
.where("date_trunc('day', slots.start_at) = :date", date: date)
|
||||
|
||||
daily_reservations = daily_reservations.where.not(id: reservation.id) unless reservation.nil?
|
||||
|
||||
(daily_reservations.map { |r| r.slots_reservations.map { |sr| sr.slot.duration } }.flatten.reduce(:+) || 0)
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.title notification.notification_type
|
||||
json.description t('.limit_reached',
|
||||
HOURS: notification.attached_object.limit,
|
||||
ITEM: notification.attached_object.limitable.name,
|
||||
DATE: I18n.l(notification.get_meta_data(:date).to_date))
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! plan, :id, :base_name, :name, :interval, :interval_count, :group_id, :training_credit_nb, :is_rolling, :description, :type,
|
||||
:ui_weight, :disabled, :monthly_payment, :plan_category_id
|
||||
:ui_weight, :disabled, :monthly_payment, :plan_category_id, :limiting, :machines_visibility
|
||||
json.amount plan.amount / 100.00
|
||||
json.prices_attributes plan.prices, partial: 'api/prices/price', as: :price
|
||||
if plan.plan_file
|
||||
@ -27,3 +27,7 @@ if plan.advanced_accounting
|
||||
end
|
||||
end
|
||||
|
||||
json.plan_limitations_attributes plan.plan_limitations do |limitation|
|
||||
json.extract! limitation, :id, :limitable_id, :limitable_type, :limit
|
||||
end
|
||||
|
||||
|
@ -1 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'api/plans/plan', plan: @plan
|
||||
|
@ -0,0 +1,9 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p>
|
||||
<%= t('.body.limit_reached',
|
||||
HOURS: @attached_object.limit,
|
||||
ITEM: @attached_object.limitable.name,
|
||||
DATE: I18n.l(@notification.get_meta_data(:date).to_date)) %>
|
||||
</p>
|
||||
|
@ -2,8 +2,8 @@ de:
|
||||
app:
|
||||
admin:
|
||||
edit_destroy_buttons:
|
||||
deleted: "The {TYPE} was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the {TYPE}: "
|
||||
deleted: "Successfully deleted."
|
||||
unable_to_delete: "Unable to delete: "
|
||||
delete_item: "Delete the {TYPE}"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this {TYPE}?"
|
||||
@ -24,7 +24,7 @@ de:
|
||||
save: "Save"
|
||||
successfully_saved: "Your banner was successfully saved."
|
||||
machine_categories_list:
|
||||
machine_categories: "Machines categories"
|
||||
machine_categories: "Maschinen-Kategorien"
|
||||
add_a_machine_category: "Add a machine category"
|
||||
name: "Name"
|
||||
machines_number: "Number of machines"
|
||||
@ -57,7 +57,7 @@ de:
|
||||
disabled_help: "When disabled, the machine won't be reservable and won't appear by default in the machines list."
|
||||
reservable: "Can this machine be reserved?"
|
||||
reservable_help: "When disabled, the machine will be shown in the default list of machines, but without the reservation button. If you already have created some availability slots for this machine, you may want to remove them: do it from the admin agenda."
|
||||
save: "Save"
|
||||
save: "Speichern"
|
||||
create_success: "The machine was created successfully"
|
||||
update_success: "The machine was updated successfully"
|
||||
training_form:
|
||||
@ -74,7 +74,7 @@ de:
|
||||
associated_machines_help: "If you associate a machine to this training, the members will need to successfully pass this training before being able to reserve the machine."
|
||||
default_seats: "Default number of seats"
|
||||
public_page: "Show in training lists"
|
||||
public_help: "When unchecked, this option will prevent the training from appearing in the trainings list."
|
||||
public_help: "Wenn diese Option deaktiviert ist, wird verhindert, dass das Training in der Trainingliste erscheint."
|
||||
disable_training: "Disable the training"
|
||||
disabled_help: "When disabled, the training won't be reservable and won't appear by default in the trainings list."
|
||||
automatic_cancellation: "Automatic cancellation"
|
||||
@ -90,39 +90,39 @@ de:
|
||||
validation_rule_info: "Define a rule that cancel an authorisation if the machines associated with the training are not reserved for a specific period of time. This rule prevails over the authorisations validity period."
|
||||
validation_rule_switch: "Activate the validation rule"
|
||||
validation_rule_period: "Time limit in months"
|
||||
save: "Save"
|
||||
create_success: "The training was created successfully"
|
||||
update_success: "The training was updated successfully"
|
||||
save: "Speichern"
|
||||
create_success: "Die Schulung wurde erfolgreich erstellt"
|
||||
update_success: "Die Schulung wurde erfolgreich aktualisiert"
|
||||
space_form:
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} space"
|
||||
watch_out_when_creating_a_new_space_its_prices_are_initialized_at_0_for_all_subscriptions: "Watch out! When creating a new space, its prices are initialized at 0 for all subscriptions."
|
||||
consider_changing_its_prices_before_creating_any_reservation_slot: "Consider changing its prices before creating any reservation slot."
|
||||
ACTION_title: "{ACTION, select, create{Neu} other{Aktualisiere den}} Raum"
|
||||
watch_out_when_creating_a_new_space_its_prices_are_initialized_at_0_for_all_subscriptions: "Achtung! Beim Erstellen eines neuen Raums wird sein Preis für alle Abonnements mit 0 angelegt."
|
||||
consider_changing_its_prices_before_creating_any_reservation_slot: "Ändern Sie ggf. die Preise, bevor Sie Reservierungs-Slots erstellen."
|
||||
name: "Name"
|
||||
illustration: "Illustration"
|
||||
description: "Description"
|
||||
characteristics: "Characteristics"
|
||||
attachments: "Attachments"
|
||||
attached_files_pdf: "Attached files (pdf)"
|
||||
add_an_attachment: "Add an attachment"
|
||||
settings: "Settings"
|
||||
default_seats: "Default number of seats"
|
||||
disable_space: "Disable the space"
|
||||
disabled_help: "When disabled, the space won't be reservable and won't appear by default in the spaces list."
|
||||
save: "Save"
|
||||
create_success: "The space was created successfully"
|
||||
update_success: "The space was updated successfully"
|
||||
illustration: "Abbildung"
|
||||
description: "Beschreibung"
|
||||
characteristics: "Eigenschaften"
|
||||
attachments: "Dateianhänge"
|
||||
attached_files_pdf: "Angehängte Dateien (pdf)"
|
||||
add_an_attachment: "Anhang hinzufügen"
|
||||
settings: "Einstellungen"
|
||||
default_seats: "Standardanzahl der Sitze"
|
||||
disable_space: "Raum deaktivieren"
|
||||
disabled_help: "Wenn deaktiviert, ist der Raum nicht reservierbar und erscheint standardmäßig nicht in der Liste der Leerzeichen."
|
||||
save: "Speichern"
|
||||
create_success: "Der Raum wurde erfolgreich erstellt"
|
||||
update_success: "Der Raum wurde erfolgreich aktualisiert"
|
||||
event_form:
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} event"
|
||||
title: "Title"
|
||||
ACTION_title: "{ACTION, select, create{Neue} other{Aktualisiere die}} Veranstaltung"
|
||||
title: "Titel"
|
||||
matching_visual: "Matching visual"
|
||||
description: "Description"
|
||||
attachments: "Attachments"
|
||||
attached_files_pdf: "Attached files (pdf)"
|
||||
add_a_new_file: "Add a new file"
|
||||
event_category: "Event category"
|
||||
event_category: "Veranstaltungskategorie"
|
||||
dates_and_opening_hours: "Dates and opening hours"
|
||||
all_day: "All day"
|
||||
all_day_help: "Will the event last all day or do you want to set times?"
|
||||
all_day_help: "Wird das Ereignis den ganzen Tag dauern oder möchtest du Zeiten festlegen?"
|
||||
start_date: "Start date"
|
||||
end_date: "End date"
|
||||
start_time: "Start time"
|
||||
@ -135,29 +135,36 @@ de:
|
||||
fare_class: "Fare class"
|
||||
price: "Price"
|
||||
seats_available: "Seats available"
|
||||
seats_help: "If you leave this field empty, this event will be available without reservations."
|
||||
event_themes: "Event themes"
|
||||
seats_help: "Wenn sie dieses Feld leer lassen, ist diese Veranstaltung ohne Reservierung."
|
||||
event_themes: "Veranstaltungsthemen"
|
||||
age_range: "Age range"
|
||||
add_price: "Add a price"
|
||||
save: "Save"
|
||||
create_success: "The event was created successfully"
|
||||
events_updated: "{COUNT, plural, =1{One event was} other{{COUNT} Events were}} successfully updated"
|
||||
events_not_updated: "{TOTAL, plural, =1{The event was} other{On {TOTAL} events {COUNT, plural, =1{one was} other{{COUNT} were}}}} not updated."
|
||||
error_deleting_reserved_price: "Unable to remove the requested price because it is associated with some existing reservations"
|
||||
other_error: "An unexpected error occurred while updating the event"
|
||||
create_success: "Die Veranstaltung wurde erfolgreich erstellt"
|
||||
events_updated: "{COUNT, plural, one {}=1{Eine Veranstaltung wurde} other{{COUNT} Veranstaltungen wurden}} erfolgreich aktualisiert"
|
||||
events_not_updated: "{TOTAL, plural, =1{Die Veranstaltung war} other{Auf {TOTAL} Veranstaltungen {COUNT, plural, =1{eins war} other{{COUNT} waren}}}} nicht aktualisiert."
|
||||
error_deleting_reserved_price: "Der angeforderte Preis konnte nicht gelöscht werden, da er mit einigen Reservierungen verknüpft ist"
|
||||
other_error: "Beim Aktualisieren der Veranstaltung ist ein unerwarteter Fehler aufgetreten"
|
||||
recurring:
|
||||
none: "None"
|
||||
every_days: "Every days"
|
||||
every_week: "Every week"
|
||||
every_month: "Every month"
|
||||
every_year: "Every year"
|
||||
every_days: "Täglich"
|
||||
every_week: "Wöchentlich"
|
||||
every_month: "Monatlich"
|
||||
every_year: "Jährlich"
|
||||
plan_form:
|
||||
general_information: "General information"
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} plan"
|
||||
tab_settings: "Einstellungen"
|
||||
tab_usage_limits: "Usage limits"
|
||||
description: "Beschreibung"
|
||||
general_settings: "Generelle Einstellungen"
|
||||
general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods."
|
||||
activation_and_payment: "Subscription activation and payment"
|
||||
name: "Name"
|
||||
name_max_length: "Name length must be less than 24 characters."
|
||||
group: "Group"
|
||||
group: "Gruppe"
|
||||
transversal: "Transversal plan"
|
||||
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups."
|
||||
display: "Display"
|
||||
category: "Category"
|
||||
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
|
||||
number_of_periods: "Number of periods"
|
||||
@ -173,10 +180,9 @@ de:
|
||||
rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought."
|
||||
monthly_payment: "Monthly payment?"
|
||||
monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months."
|
||||
description: "Description"
|
||||
information_sheet: "Information sheet"
|
||||
notified_partner: "Notified partner"
|
||||
new_user: "New user ..."
|
||||
new_user: "New user"
|
||||
alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
disabled: "Disable subscription"
|
||||
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
@ -185,9 +191,39 @@ de:
|
||||
partner_plan: "Partner plan"
|
||||
partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan."
|
||||
partner_created: "The partner was successfully created"
|
||||
ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan"
|
||||
slots_visibility: "Slots visibility"
|
||||
slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings."
|
||||
machines_visibility: "Visibility time limit, in hours (machines)"
|
||||
save: "Save"
|
||||
create_success: "Plan(s) successfully created. Don't forget to redefine prices."
|
||||
update_success: "The plan was updated successfully"
|
||||
plan_limit_form:
|
||||
usage_limitation: "Limitation of use"
|
||||
usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation."
|
||||
usage_limitation_switch: "Restrict machine reservations to a number of hours per day."
|
||||
new_usage_limitation: "Add a limitation of use"
|
||||
all_limitations: "All limitations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
max_hours_per_day: "Max. hours/day"
|
||||
ongoing_limitations: "Ongoing limitations"
|
||||
saved_limitations: "Saved limitations"
|
||||
cancel: "Cancel this limitation"
|
||||
cancel_deletion: "Cancel"
|
||||
ongoing_deletion: "Ongoing deletion"
|
||||
plan_limit_modal:
|
||||
title: "Manage limitation of use"
|
||||
limit_reservations: "Limit reservations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
categories_info: "If you select all machine categories, the limits will apply across the board."
|
||||
machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten."
|
||||
max_hours_per_day: "Maximum number of reservation hours per day"
|
||||
confirm: "Confirm"
|
||||
partner_modal:
|
||||
title: "Create a new partner"
|
||||
create_partner: "Create the partner"
|
||||
@ -196,16 +232,17 @@ de:
|
||||
email: "Email address"
|
||||
plan_pricing_form:
|
||||
prices: "Prices"
|
||||
about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour."
|
||||
copy_prices_from: "Copy prices from"
|
||||
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
|
||||
machines: "Machines"
|
||||
spaces: "Spaces"
|
||||
update_recurrent_modal:
|
||||
title: "Periodic event update"
|
||||
edit_recurring_event: "You're about to update a periodic event. What do you want to update?"
|
||||
edit_this_event: "Only this event"
|
||||
edit_recurring_event: "Sie bearbeiten eine wiederkehrende Veranstaltung. Was möchten Sie ändern?"
|
||||
edit_this_event: "Nur diese Veranstaltung"
|
||||
edit_this_and_next: "This event and the followings"
|
||||
edit_all: "All events"
|
||||
edit_all: "Alle Veranstaltungen"
|
||||
date_wont_change: "Warning: you have changed the event date. This modification won't be propagated to other occurrences of the periodic event."
|
||||
confirm: "Update the {MODE, select, single{event} other{events}}"
|
||||
advanced_accounting_form:
|
||||
@ -227,7 +264,7 @@ de:
|
||||
subscriptions: "Subscriptions"
|
||||
machine: "Machine reservation"
|
||||
training: "Training reservation"
|
||||
event: "Event reservation"
|
||||
event: "Veranstaltungsreservierung"
|
||||
space: "Space reservation"
|
||||
prepaid_pack: "Pack of prepaid-hours"
|
||||
product: "Product of the store"
|
||||
@ -286,7 +323,7 @@ de:
|
||||
manage_trainings: "Klicke hier, um Schulungen hinzuzufügen oder zu entfernen."
|
||||
number_of_tickets: "Anzahl der Tickets: "
|
||||
adjust_the_opening_hours: "Öffnungszeiten anpassen"
|
||||
to_time: "bis" #eg. from 18:00 to 21:00
|
||||
to_time: "bis" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "Einschränkungsoptionen"
|
||||
restrict_with_labels: "Diesen Slot mit Labels einschränken"
|
||||
restrict_for_subscriptions: "Diesen Slot auf Abonnenten einschränken"
|
||||
@ -508,8 +545,8 @@ de:
|
||||
on_DATE: "am {DATE}"
|
||||
from_DATE: "von {DATE}"
|
||||
from_TIME: "ab {TIME}"
|
||||
to_date: "bis" #eg: from 01/01 to 01/05
|
||||
to_time: "bis" #eg. from 18:00 to 21:00
|
||||
to_date: "bis" #e.g.: from 01/01 to 01/05
|
||||
to_time: "bis" #e.g. from 18:00 to 21:00
|
||||
title: "Titel"
|
||||
dates: "Datum"
|
||||
booking: "Buchung"
|
||||
@ -586,13 +623,13 @@ de:
|
||||
events_settings:
|
||||
title: "Settings"
|
||||
generic_text_block: "Editorial text block"
|
||||
generic_text_block_info: "Displays an editorial block above the list of events visible to members."
|
||||
generic_text_block_info: "Zeigt einen redaktionellen Block oberhalb der für Mitglieder sichtbaren Veranstaltungsliste."
|
||||
generic_text_block_switch: "Display editorial block"
|
||||
cta_switch: "Display a button"
|
||||
cta_label: "Button label"
|
||||
cta_url: "url"
|
||||
save: "Save"
|
||||
update_success: "The events settings were successfully updated"
|
||||
update_success: "Die Einstellungen wurden erfolgreich aktualisiert"
|
||||
#subscriptions, prices, credits and coupons management
|
||||
pricing:
|
||||
pricing_management: "Preisverwaltung"
|
||||
@ -1453,8 +1490,8 @@ de:
|
||||
statistics: "Statistiken"
|
||||
evolution: "Entwicklung"
|
||||
age_filter: "Altersfilter"
|
||||
from_age: "Von" #eg. from 8 to 40 years old
|
||||
to_age: "bis" #eg. from 8 to 40 years old
|
||||
from_age: "Von" #e.g. from 8 to 40 years old
|
||||
to_age: "bis" #e.g. from 8 to 40 years old
|
||||
start: "Start:"
|
||||
end: "Ende:"
|
||||
custom_filter: "Benutzerderfinierter Filter"
|
||||
@ -2215,8 +2252,6 @@ de:
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
all_products: "All products"
|
||||
create_a_product: "Create a product"
|
||||
successfully_deleted: "The product has been successfully deleted"
|
||||
unable_to_delete: "Unable to delete the product: "
|
||||
filter: "Filter"
|
||||
filter_clear: "Clear all"
|
||||
filter_apply: "Apply"
|
||||
@ -2238,6 +2273,7 @@ de:
|
||||
sort: "Sort:"
|
||||
visible_only: "Visible products only"
|
||||
product_item:
|
||||
product: "product"
|
||||
visible: "visible"
|
||||
hidden: "hidden"
|
||||
stock:
|
||||
@ -2288,12 +2324,13 @@ de:
|
||||
low_stock: "Low stock"
|
||||
threshold_level: "Minimum threshold level"
|
||||
threshold_alert: "Notify me when the threshold is reached"
|
||||
events_history: "Events history"
|
||||
event_type: "Events:"
|
||||
events_history: "Veranstaltungsverlauf"
|
||||
event_type: "Veranstaltungen:"
|
||||
reason: "Reason"
|
||||
stocks: "Stock:"
|
||||
internal: "Private stock"
|
||||
external: "Public stock"
|
||||
edit: "Edit"
|
||||
all: "All types"
|
||||
remaining_stock: "Remaining stock"
|
||||
type_in: "Add"
|
||||
@ -2385,7 +2422,7 @@ de:
|
||||
VAT_rate_machine: "Machine reservation"
|
||||
VAT_rate_space: "Space reservation"
|
||||
VAT_rate_training: "Training reservation"
|
||||
VAT_rate_event: "Event reservation"
|
||||
VAT_rate_event: "Veranstaltungsreservierung"
|
||||
VAT_rate_subscription: "Subscription"
|
||||
VAT_rate_product: "Products (store)"
|
||||
multi_VAT_notice: "<strong>Please note</strong>: The current general rate is {RATE}%. You can define different VAT rates for each category.<br><br>For example, you can override this value, only for machine reservations, by filling in the corresponding field beside. If you don't fill any value, the general rate will apply."
|
||||
|
@ -2,8 +2,8 @@ en:
|
||||
app:
|
||||
admin:
|
||||
edit_destroy_buttons:
|
||||
deleted: "The {TYPE} was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the {TYPE}: "
|
||||
deleted: "Successfully deleted."
|
||||
unable_to_delete: "Unable to delete: "
|
||||
delete_item: "Delete the {TYPE}"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this {TYPE}?"
|
||||
@ -152,12 +152,19 @@ en:
|
||||
every_month: "Every month"
|
||||
every_year: "Every year"
|
||||
plan_form:
|
||||
general_information: "General information"
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} plan"
|
||||
tab_settings: "Settings"
|
||||
tab_usage_limits: "Usage limits"
|
||||
description: "Description"
|
||||
general_settings: "General settings"
|
||||
general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods."
|
||||
activation_and_payment: "Subscription activation and payment"
|
||||
name: "Name"
|
||||
name_max_length: "Name length must be less than 24 characters."
|
||||
group: "Group"
|
||||
transversal: "Transversal plan"
|
||||
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups."
|
||||
display: "Display"
|
||||
category: "Category"
|
||||
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
|
||||
number_of_periods: "Number of periods"
|
||||
@ -173,10 +180,9 @@ en:
|
||||
rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought."
|
||||
monthly_payment: "Monthly payment?"
|
||||
monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months."
|
||||
description: "Description"
|
||||
information_sheet: "Information sheet"
|
||||
notified_partner: "Notified partner"
|
||||
new_user: "New user ..."
|
||||
new_user: "New user"
|
||||
alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
disabled: "Disable subscription"
|
||||
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
@ -185,9 +191,39 @@ en:
|
||||
partner_plan: "Partner plan"
|
||||
partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan."
|
||||
partner_created: "The partner was successfully created"
|
||||
ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan"
|
||||
slots_visibility: "Slots visibility"
|
||||
slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings."
|
||||
machines_visibility: "Visibility time limit, in hours (machines)"
|
||||
save: "Save"
|
||||
create_success: "Plan(s) successfully created. Don't forget to redefine prices."
|
||||
update_success: "The plan was updated successfully"
|
||||
plan_limit_form:
|
||||
usage_limitation: "Limitation of use"
|
||||
usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation."
|
||||
usage_limitation_switch: "Restrict machine reservations to a number of hours per day."
|
||||
new_usage_limitation: "Add a limitation of use"
|
||||
all_limitations: "All limitations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
max_hours_per_day: "Max. hours/day"
|
||||
ongoing_limitations: "Ongoing limitations"
|
||||
saved_limitations: "Saved limitations"
|
||||
cancel: "Cancel this limitation"
|
||||
cancel_deletion: "Cancel"
|
||||
ongoing_deletion: "Ongoing deletion"
|
||||
plan_limit_modal:
|
||||
title: "Manage limitation of use"
|
||||
limit_reservations: "Limit reservations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
categories_info: "If you select all machine categories, the limits will apply across the board."
|
||||
machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten."
|
||||
max_hours_per_day: "Maximum number of reservation hours per day"
|
||||
confirm: "Confirm"
|
||||
partner_modal:
|
||||
title: "Create a new partner"
|
||||
create_partner: "Create the partner"
|
||||
@ -196,6 +232,7 @@ en:
|
||||
email: "Email address"
|
||||
plan_pricing_form:
|
||||
prices: "Prices"
|
||||
about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour."
|
||||
copy_prices_from: "Copy prices from"
|
||||
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
|
||||
machines: "Machines"
|
||||
@ -286,7 +323,7 @@ en:
|
||||
manage_trainings: "Click here to add or remove trainings."
|
||||
number_of_tickets: "Number of tickets: "
|
||||
adjust_the_opening_hours: "Adjust the opening hours"
|
||||
to_time: "to" #eg. from 18:00 to 21:00
|
||||
to_time: "to" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "Restriction options"
|
||||
restrict_with_labels: "Restrict this slot with labels"
|
||||
restrict_for_subscriptions: "Restrict this slot for subscription users"
|
||||
@ -508,8 +545,8 @@ en:
|
||||
on_DATE: "on {DATE}"
|
||||
from_DATE: "from {DATE}"
|
||||
from_TIME: "from {TIME}"
|
||||
to_date: "to" #eg: from 01/01 to 01/05
|
||||
to_time: "to" #eg. from 18:00 to 21:00
|
||||
to_date: "to" #e.g.: from 01/01 to 01/05
|
||||
to_time: "to" #e.g. from 18:00 to 21:00
|
||||
title: "Title"
|
||||
dates: "Dates"
|
||||
booking: "Booking"
|
||||
@ -1453,8 +1490,8 @@ en:
|
||||
statistics: "Statistics"
|
||||
evolution: "Evolution"
|
||||
age_filter: "Age filter"
|
||||
from_age: "From" #eg. from 8 to 40 years old
|
||||
to_age: "to" #eg. from 8 to 40 years old
|
||||
from_age: "From" #e.g. from 8 to 40 years old
|
||||
to_age: "to" #e.g. from 8 to 40 years old
|
||||
start: "Start:"
|
||||
end: "End:"
|
||||
custom_filter: "Custom filter"
|
||||
@ -2215,8 +2252,6 @@ en:
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
all_products: "All products"
|
||||
create_a_product: "Create a product"
|
||||
successfully_deleted: "The product has been successfully deleted"
|
||||
unable_to_delete: "Unable to delete the product: "
|
||||
filter: "Filter"
|
||||
filter_clear: "Clear all"
|
||||
filter_apply: "Apply"
|
||||
@ -2238,6 +2273,7 @@ en:
|
||||
sort: "Sort:"
|
||||
visible_only: "Visible products only"
|
||||
product_item:
|
||||
product: "product"
|
||||
visible: "visible"
|
||||
hidden: "hidden"
|
||||
stock:
|
||||
@ -2294,6 +2330,7 @@ en:
|
||||
stocks: "Stock:"
|
||||
internal: "Private stock"
|
||||
external: "Public stock"
|
||||
edit: "Edit"
|
||||
all: "All types"
|
||||
remaining_stock: "Remaining stock"
|
||||
type_in: "Add"
|
||||
|
@ -2,8 +2,8 @@ es:
|
||||
app:
|
||||
admin:
|
||||
edit_destroy_buttons:
|
||||
deleted: "The {TYPE} was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the {TYPE}: "
|
||||
deleted: "Successfully deleted."
|
||||
unable_to_delete: "Unable to delete: "
|
||||
delete_item: "Delete the {TYPE}"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this {TYPE}?"
|
||||
@ -152,12 +152,19 @@ es:
|
||||
every_month: "Every month"
|
||||
every_year: "Every year"
|
||||
plan_form:
|
||||
general_information: "General information"
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} plan"
|
||||
tab_settings: "Settings"
|
||||
tab_usage_limits: "Usage limits"
|
||||
description: "Description"
|
||||
general_settings: "General settings"
|
||||
general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods."
|
||||
activation_and_payment: "Subscription activation and payment"
|
||||
name: "Name"
|
||||
name_max_length: "Name length must be less than 24 characters."
|
||||
group: "Group"
|
||||
transversal: "Transversal plan"
|
||||
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups."
|
||||
display: "Display"
|
||||
category: "Category"
|
||||
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
|
||||
number_of_periods: "Number of periods"
|
||||
@ -173,10 +180,9 @@ es:
|
||||
rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought."
|
||||
monthly_payment: "Monthly payment?"
|
||||
monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months."
|
||||
description: "Description"
|
||||
information_sheet: "Information sheet"
|
||||
notified_partner: "Notified partner"
|
||||
new_user: "New user ..."
|
||||
new_user: "New user"
|
||||
alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
disabled: "Disable subscription"
|
||||
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
@ -185,9 +191,39 @@ es:
|
||||
partner_plan: "Partner plan"
|
||||
partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan."
|
||||
partner_created: "The partner was successfully created"
|
||||
ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan"
|
||||
slots_visibility: "Slots visibility"
|
||||
slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings."
|
||||
machines_visibility: "Visibility time limit, in hours (machines)"
|
||||
save: "Save"
|
||||
create_success: "Plan(s) successfully created. Don't forget to redefine prices."
|
||||
update_success: "The plan was updated successfully"
|
||||
plan_limit_form:
|
||||
usage_limitation: "Limitation of use"
|
||||
usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation."
|
||||
usage_limitation_switch: "Restrict machine reservations to a number of hours per day."
|
||||
new_usage_limitation: "Add a limitation of use"
|
||||
all_limitations: "All limitations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
max_hours_per_day: "Max. hours/day"
|
||||
ongoing_limitations: "Ongoing limitations"
|
||||
saved_limitations: "Saved limitations"
|
||||
cancel: "Cancel this limitation"
|
||||
cancel_deletion: "Cancel"
|
||||
ongoing_deletion: "Ongoing deletion"
|
||||
plan_limit_modal:
|
||||
title: "Manage limitation of use"
|
||||
limit_reservations: "Limit reservations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
categories_info: "If you select all machine categories, the limits will apply across the board."
|
||||
machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten."
|
||||
max_hours_per_day: "Maximum number of reservation hours per day"
|
||||
confirm: "Confirm"
|
||||
partner_modal:
|
||||
title: "Create a new partner"
|
||||
create_partner: "Create the partner"
|
||||
@ -196,6 +232,7 @@ es:
|
||||
email: "Email address"
|
||||
plan_pricing_form:
|
||||
prices: "Prices"
|
||||
about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour."
|
||||
copy_prices_from: "Copy prices from"
|
||||
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
|
||||
machines: "Machines"
|
||||
@ -286,7 +323,7 @@ es:
|
||||
manage_trainings: "Click here to add or remove trainings."
|
||||
number_of_tickets: "Número de tickets: "
|
||||
adjust_the_opening_hours: "Ajustar el horario de apertura"
|
||||
to_time: "a" #eg. from 18:00 to 21:00
|
||||
to_time: "a" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "Restriction options"
|
||||
restrict_with_labels: "Restringir este horario con etiquetas"
|
||||
restrict_for_subscriptions: "Restrict this slot for subscription users"
|
||||
@ -508,8 +545,8 @@ es:
|
||||
on_DATE: "on {DATE}"
|
||||
from_DATE: "Desde {DATE}"
|
||||
from_TIME: "Desde {TIME}"
|
||||
to_date: "to" #eg: from 01/01 to 01/05
|
||||
to_time: "to" #eg. from 18:00 to 21:00
|
||||
to_date: "to" #e.g.: from 01/01 to 01/05
|
||||
to_time: "to" #e.g. from 18:00 to 21:00
|
||||
title: "Title"
|
||||
dates: "Dates"
|
||||
booking: "Booking"
|
||||
@ -1453,8 +1490,8 @@ es:
|
||||
statistics: "Statistics"
|
||||
evolution: "Evolución"
|
||||
age_filter: "Filtro de edad"
|
||||
from_age: "Desde" #eg. from 8 to 40 years old
|
||||
to_age: "a" #eg. from 8 to 40 years old
|
||||
from_age: "Desde" #e.g. from 8 to 40 years old
|
||||
to_age: "a" #e.g. from 8 to 40 years old
|
||||
start: "Principio:"
|
||||
end: "Final:"
|
||||
custom_filter: "Filtro personalizado"
|
||||
@ -2215,8 +2252,6 @@ es:
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
all_products: "All products"
|
||||
create_a_product: "Create a product"
|
||||
successfully_deleted: "The product has been successfully deleted"
|
||||
unable_to_delete: "Unable to delete the product: "
|
||||
filter: "Filter"
|
||||
filter_clear: "Clear all"
|
||||
filter_apply: "Apply"
|
||||
@ -2238,6 +2273,7 @@ es:
|
||||
sort: "Sort:"
|
||||
visible_only: "Visible products only"
|
||||
product_item:
|
||||
product: "product"
|
||||
visible: "visible"
|
||||
hidden: "hidden"
|
||||
stock:
|
||||
@ -2294,6 +2330,7 @@ es:
|
||||
stocks: "Stock:"
|
||||
internal: "Private stock"
|
||||
external: "Public stock"
|
||||
edit: "Edit"
|
||||
all: "All types"
|
||||
remaining_stock: "Remaining stock"
|
||||
type_in: "Add"
|
||||
|
@ -152,12 +152,19 @@ fr:
|
||||
every_month: "Chaque mois"
|
||||
every_year: "Chaque année"
|
||||
plan_form:
|
||||
general_information: "Informations générales"
|
||||
ACTION_title: "{ACTION, select, create{Nouvelle} other{Mettre à jour la}} formule d'abonnement"
|
||||
tab_settings: "Paramètres"
|
||||
tab_usage_limits: "Limites d'utilisation"
|
||||
description: "Description"
|
||||
general_settings: "Paramètres généraux"
|
||||
general_settings_info: "Déterminez à quel groupe cet abonnement est dédié. Définissez également son prix et sa durée en périodes."
|
||||
activation_and_payment: "Activation et paiement de l'abonnement"
|
||||
name: "Nom"
|
||||
name_max_length: "Le nom doit faire moins de 24 caractères."
|
||||
group: "Groupe"
|
||||
transversal: "Abonnement transversal"
|
||||
transversal_help: "Si cette option est cochée, une copie de cette formule d'abonnement sera créée pour chaque groupe actuellement activé."
|
||||
display: "Affichage"
|
||||
category: "Catégorie"
|
||||
category_help: "Les catégories vous permettent de regrouper les formules d'abonnement, sur la vue publique des abonnements."
|
||||
number_of_periods: "Nombre de périodes"
|
||||
@ -173,7 +180,6 @@ fr:
|
||||
rolling_subscription_help: "Un abonnement glissant commencera le jour de la première formation. Sinon, il commencera dès qu'il est acheté."
|
||||
monthly_payment: "Paiement mensuel ?"
|
||||
monthly_payment_help: "Si le paiement mensuel est activé, les membres pourront choisir entre un paiement unique ou un échéancier de paiement échelonné chaque mois."
|
||||
description: "Description"
|
||||
information_sheet: "Fiche descriptive"
|
||||
notified_partner: "Partenaire notifié"
|
||||
new_user: "Nouvel utilisateur ..."
|
||||
@ -185,9 +191,39 @@ fr:
|
||||
partner_plan: "Abonnement partenaire"
|
||||
partner_plan_help: "Vous pouvez vendre des abonnements en partenariat avec un autre organisme. Ce faisant, l'autre entité sera informée lorsqu'un membre s'abonne à cette formule d'abonnement."
|
||||
partner_created: "Le partenaire a bien été créé"
|
||||
ACTION_plan: "{ACTION, select, create{Créer} other{Mettre à jour}} la formule d'abonnement"
|
||||
slots_visibility: "Visibilité des créneaux"
|
||||
slots_visibility_help: "Vous pouvez déterminer combien de temps en avance les abonnés peuvent voir et réserver les créneaux machines. Lorsque ce paramètre est défini, il devient prioritaire sur les paramètres généraux."
|
||||
machines_visibility: "Délai de visibilité, en heures (machines)"
|
||||
save: "Enregistrer"
|
||||
create_success: "Création du/des formule(s) d'abonnement réussie(s). N'oubliez pas de redéfinir les tarifs."
|
||||
update_success: "La formule d'abonnement a bien été mise à jour"
|
||||
plan_limit_form:
|
||||
usage_limitation: "Limite d'usage"
|
||||
usage_limitation_info: "Définissez un nombre maximum d'heures de réservation par jour et par catégorie de machine. Les catégories de machines qui n'ont aucun paramètre configuré ne seront soumises à aucune limitation."
|
||||
usage_limitation_switch: "Restreindre les réservations de machines à un certain nombre d'heures par jour."
|
||||
new_usage_limitation: "Ajouter une limite d'usage"
|
||||
all_limitations: "Toutes les limitations"
|
||||
by_category: "Par catégorie de machines"
|
||||
by_machine: "Par machine"
|
||||
category: "Catégorie de machines"
|
||||
machine: "Nom machine"
|
||||
max_hours_per_day: "Max. heures/jour"
|
||||
ongoing_limitations: "Limites en cours"
|
||||
saved_limitations: "Limites enregistrées"
|
||||
cancel: "Annuler cette limite"
|
||||
cancel_deletion: "Annuler"
|
||||
ongoing_deletion: "Suppression en cours"
|
||||
plan_limit_modal:
|
||||
title: "Gérer la limite d'usage"
|
||||
limit_reservations: "Limiter les réservations"
|
||||
by_category: "Par catégorie de machines"
|
||||
by_machine: "Par machine"
|
||||
category: "Catégorie de machines"
|
||||
machine: "Nom machine"
|
||||
categories_info: "Si vous sélectionnez toutes les catégories de machines, les limites s'appliqueront à tous les niveaux."
|
||||
machine_info: "Veuillez noter que si vous avez déjà créé une limite pour la catégorie de machines incluant la machine sélectionnée, elle sera définitivement écrasée."
|
||||
max_hours_per_day: "Nombre maximum d'heures de réservation par jour"
|
||||
confirm: "Confirmer"
|
||||
partner_modal:
|
||||
title: "Créer un nouveau partenaire"
|
||||
create_partner: "Créer le partenaire"
|
||||
@ -196,6 +232,7 @@ fr:
|
||||
email: "Adresse électronique"
|
||||
plan_pricing_form:
|
||||
prices: "Tarifs"
|
||||
about_prices: "Les prix définis ici s'appliqueront aux membres qui s'abonneront à cette formule d'abonnement, pour les machines et les espaces."
|
||||
copy_prices_from: "Copier les prix depuis"
|
||||
copy_prices_from_help: "Cela remplacera tous les prix de cette formule d'abonnement par les prix de la formule sélectionnée"
|
||||
machines: "Machines"
|
||||
@ -286,7 +323,7 @@ fr:
|
||||
manage_trainings: "Cliquez-ici pour ajouter ou supprimer des formations."
|
||||
number_of_tickets: "Nombre de places : "
|
||||
adjust_the_opening_hours: "Ajuster l'horaire"
|
||||
to_time: "à" #eg. from 18:00 to 21:00
|
||||
to_time: "à" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "Options de restriction"
|
||||
restrict_with_labels: "Restreindre ce créneau avec des étiquettes"
|
||||
restrict_for_subscriptions: "Restreindre ce créneau pour les abonnements"
|
||||
@ -508,8 +545,8 @@ fr:
|
||||
on_DATE: "le {DATE}"
|
||||
from_DATE: "du {DATE}"
|
||||
from_TIME: "de {TIME}"
|
||||
to_date: "au" #eg: from 01/01 to 01/05
|
||||
to_time: "à" #eg. from 18:00 to 21:00
|
||||
to_date: "au" #e.g.: from 01/01 to 01/05
|
||||
to_time: "à" #e.g. from 18:00 to 21:00
|
||||
title: "Titre"
|
||||
dates: "Dates"
|
||||
booking: "Réservations"
|
||||
@ -1453,8 +1490,8 @@ fr:
|
||||
statistics: "Statistiques"
|
||||
evolution: "Évolution"
|
||||
age_filter: "Filtre d'âge"
|
||||
from_age: "De" #eg. from 8 to 40 years old
|
||||
to_age: "à" #eg. from 8 to 40 years old
|
||||
from_age: "De" #e.g. from 8 to 40 years old
|
||||
to_age: "à" #e.g. from 8 to 40 years old
|
||||
start: "Début :"
|
||||
end: "Fin :"
|
||||
custom_filter: "Filtre personnalisé"
|
||||
@ -2215,8 +2252,6 @@ fr:
|
||||
unexpected_error_occurred: "Une erreur inattendue s'est produite. Veuillez réessayer ultérieurement."
|
||||
all_products: "Tous les produits"
|
||||
create_a_product: "Créer un produit"
|
||||
successfully_deleted: "Le produit a bien été supprimé"
|
||||
unable_to_delete: "Impossible de supprimer le produit : "
|
||||
filter: "Filter"
|
||||
filter_clear: "Tout effacer"
|
||||
filter_apply: "Appliquer"
|
||||
@ -2238,6 +2273,7 @@ fr:
|
||||
sort: "Trier :"
|
||||
visible_only: "Produits visibles uniquement"
|
||||
product_item:
|
||||
product: "produit"
|
||||
visible: "visible"
|
||||
hidden: "caché"
|
||||
stock:
|
||||
@ -2294,6 +2330,7 @@ fr:
|
||||
stocks: "Stock :"
|
||||
internal: "Stock interne"
|
||||
external: "Stock externe"
|
||||
edit: "Modifier"
|
||||
all: "Tous types"
|
||||
remaining_stock: "Stock restant"
|
||||
type_in: "Ajouter"
|
||||
|
@ -2,8 +2,8 @@
|
||||
app:
|
||||
admin:
|
||||
edit_destroy_buttons:
|
||||
deleted: "The {TYPE} was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the {TYPE}: "
|
||||
deleted: "Successfully deleted."
|
||||
unable_to_delete: "Unable to delete: "
|
||||
delete_item: "Delete the {TYPE}"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this {TYPE}?"
|
||||
@ -152,12 +152,19 @@
|
||||
every_month: "Every month"
|
||||
every_year: "Every year"
|
||||
plan_form:
|
||||
general_information: "General information"
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} plan"
|
||||
tab_settings: "Settings"
|
||||
tab_usage_limits: "Usage limits"
|
||||
description: "Description"
|
||||
general_settings: "General settings"
|
||||
general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods."
|
||||
activation_and_payment: "Subscription activation and payment"
|
||||
name: "Name"
|
||||
name_max_length: "Name length must be less than 24 characters."
|
||||
group: "Group"
|
||||
transversal: "Transversal plan"
|
||||
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups."
|
||||
display: "Display"
|
||||
category: "Category"
|
||||
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
|
||||
number_of_periods: "Number of periods"
|
||||
@ -173,10 +180,9 @@
|
||||
rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought."
|
||||
monthly_payment: "Monthly payment?"
|
||||
monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months."
|
||||
description: "Description"
|
||||
information_sheet: "Information sheet"
|
||||
notified_partner: "Notified partner"
|
||||
new_user: "New user ..."
|
||||
new_user: "New user"
|
||||
alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
disabled: "Disable subscription"
|
||||
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
@ -185,9 +191,39 @@
|
||||
partner_plan: "Partner plan"
|
||||
partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan."
|
||||
partner_created: "The partner was successfully created"
|
||||
ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan"
|
||||
slots_visibility: "Slots visibility"
|
||||
slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings."
|
||||
machines_visibility: "Visibility time limit, in hours (machines)"
|
||||
save: "Save"
|
||||
create_success: "Plan(s) successfully created. Don't forget to redefine prices."
|
||||
update_success: "The plan was updated successfully"
|
||||
plan_limit_form:
|
||||
usage_limitation: "Limitation of use"
|
||||
usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation."
|
||||
usage_limitation_switch: "Restrict machine reservations to a number of hours per day."
|
||||
new_usage_limitation: "Add a limitation of use"
|
||||
all_limitations: "All limitations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
max_hours_per_day: "Max. hours/day"
|
||||
ongoing_limitations: "Ongoing limitations"
|
||||
saved_limitations: "Saved limitations"
|
||||
cancel: "Cancel this limitation"
|
||||
cancel_deletion: "Cancel"
|
||||
ongoing_deletion: "Ongoing deletion"
|
||||
plan_limit_modal:
|
||||
title: "Manage limitation of use"
|
||||
limit_reservations: "Limit reservations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
categories_info: "If you select all machine categories, the limits will apply across the board."
|
||||
machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten."
|
||||
max_hours_per_day: "Maximum number of reservation hours per day"
|
||||
confirm: "Confirm"
|
||||
partner_modal:
|
||||
title: "Create a new partner"
|
||||
create_partner: "Create the partner"
|
||||
@ -196,6 +232,7 @@
|
||||
email: "Email address"
|
||||
plan_pricing_form:
|
||||
prices: "Prices"
|
||||
about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour."
|
||||
copy_prices_from: "Copy prices from"
|
||||
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
|
||||
machines: "Machines"
|
||||
@ -286,7 +323,7 @@
|
||||
manage_trainings: "Klikk her for å legge til eller endre opplæring."
|
||||
number_of_tickets: "Antall billetter: "
|
||||
adjust_the_opening_hours: "Endre åpningstid"
|
||||
to_time: "til" #eg. from 18:00 to 21:00
|
||||
to_time: "til" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "Alternativer for begrensning"
|
||||
restrict_with_labels: "Begrens denne reservasjonen med etiketter"
|
||||
restrict_for_subscriptions: "Begrens denne reservasjoen til medlemmer"
|
||||
@ -508,8 +545,8 @@
|
||||
on_DATE: "{DATE}"
|
||||
from_DATE: "fra {DATE}"
|
||||
from_TIME: "fra {TIME}"
|
||||
to_date: "til" #eg: from 01/01 to 01/05
|
||||
to_time: "til" #eg. from 18:00 to 21:00
|
||||
to_date: "til" #e.g.: from 01/01 to 01/05
|
||||
to_time: "til" #e.g. from 18:00 to 21:00
|
||||
title: "Tittel"
|
||||
dates: "Datoer"
|
||||
booking: "Reservasjon"
|
||||
@ -1453,8 +1490,8 @@
|
||||
statistics: "Statistikk"
|
||||
evolution: "Utvikling"
|
||||
age_filter: "Aldersfilter"
|
||||
from_age: "Fra" #eg. from 8 to 40 years old
|
||||
to_age: "til" #eg. from 8 to 40 years old
|
||||
from_age: "Fra" #e.g. from 8 to 40 years old
|
||||
to_age: "til" #e.g. from 8 to 40 years old
|
||||
start: "Start:"
|
||||
end: "Slutt:"
|
||||
custom_filter: "Egendefinerte filtre"
|
||||
@ -2215,8 +2252,6 @@
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
all_products: "All products"
|
||||
create_a_product: "Create a product"
|
||||
successfully_deleted: "The product has been successfully deleted"
|
||||
unable_to_delete: "Unable to delete the product: "
|
||||
filter: "Filter"
|
||||
filter_clear: "Clear all"
|
||||
filter_apply: "Apply"
|
||||
@ -2238,6 +2273,7 @@
|
||||
sort: "Sort:"
|
||||
visible_only: "Visible products only"
|
||||
product_item:
|
||||
product: "product"
|
||||
visible: "visible"
|
||||
hidden: "hidden"
|
||||
stock:
|
||||
@ -2294,6 +2330,7 @@
|
||||
stocks: "Stock:"
|
||||
internal: "Private stock"
|
||||
external: "Public stock"
|
||||
edit: "Edit"
|
||||
all: "All types"
|
||||
remaining_stock: "Remaining stock"
|
||||
type_in: "Add"
|
||||
|
@ -2,8 +2,8 @@ pt:
|
||||
app:
|
||||
admin:
|
||||
edit_destroy_buttons:
|
||||
deleted: "The {TYPE} was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the {TYPE}: "
|
||||
deleted: "Successfully deleted."
|
||||
unable_to_delete: "Unable to delete: "
|
||||
delete_item: "Delete the {TYPE}"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this {TYPE}?"
|
||||
@ -152,12 +152,19 @@ pt:
|
||||
every_month: "Every month"
|
||||
every_year: "Every year"
|
||||
plan_form:
|
||||
general_information: "General information"
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} plan"
|
||||
tab_settings: "Settings"
|
||||
tab_usage_limits: "Usage limits"
|
||||
description: "Description"
|
||||
general_settings: "General settings"
|
||||
general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods."
|
||||
activation_and_payment: "Subscription activation and payment"
|
||||
name: "Name"
|
||||
name_max_length: "Name length must be less than 24 characters."
|
||||
group: "Group"
|
||||
transversal: "Transversal plan"
|
||||
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups."
|
||||
display: "Display"
|
||||
category: "Category"
|
||||
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
|
||||
number_of_periods: "Number of periods"
|
||||
@ -173,10 +180,9 @@ pt:
|
||||
rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought."
|
||||
monthly_payment: "Monthly payment?"
|
||||
monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months."
|
||||
description: "Description"
|
||||
information_sheet: "Information sheet"
|
||||
notified_partner: "Notified partner"
|
||||
new_user: "New user ..."
|
||||
new_user: "New user"
|
||||
alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
disabled: "Disable subscription"
|
||||
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
@ -185,9 +191,39 @@ pt:
|
||||
partner_plan: "Partner plan"
|
||||
partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan."
|
||||
partner_created: "The partner was successfully created"
|
||||
ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan"
|
||||
slots_visibility: "Slots visibility"
|
||||
slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings."
|
||||
machines_visibility: "Visibility time limit, in hours (machines)"
|
||||
save: "Save"
|
||||
create_success: "Plan(s) successfully created. Don't forget to redefine prices."
|
||||
update_success: "The plan was updated successfully"
|
||||
plan_limit_form:
|
||||
usage_limitation: "Limitation of use"
|
||||
usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation."
|
||||
usage_limitation_switch: "Restrict machine reservations to a number of hours per day."
|
||||
new_usage_limitation: "Add a limitation of use"
|
||||
all_limitations: "All limitations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
max_hours_per_day: "Max. hours/day"
|
||||
ongoing_limitations: "Ongoing limitations"
|
||||
saved_limitations: "Saved limitations"
|
||||
cancel: "Cancel this limitation"
|
||||
cancel_deletion: "Cancel"
|
||||
ongoing_deletion: "Ongoing deletion"
|
||||
plan_limit_modal:
|
||||
title: "Manage limitation of use"
|
||||
limit_reservations: "Limit reservations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
categories_info: "If you select all machine categories, the limits will apply across the board."
|
||||
machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten."
|
||||
max_hours_per_day: "Maximum number of reservation hours per day"
|
||||
confirm: "Confirm"
|
||||
partner_modal:
|
||||
title: "Create a new partner"
|
||||
create_partner: "Create the partner"
|
||||
@ -196,6 +232,7 @@ pt:
|
||||
email: "Email address"
|
||||
plan_pricing_form:
|
||||
prices: "Prices"
|
||||
about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour."
|
||||
copy_prices_from: "Copy prices from"
|
||||
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
|
||||
machines: "Machines"
|
||||
@ -286,7 +323,7 @@ pt:
|
||||
manage_trainings: "Clique aqui para adicionar ou remover treinamentos."
|
||||
number_of_tickets: "Número de vagas: "
|
||||
adjust_the_opening_hours: "Ajustar o horário de funcionamento"
|
||||
to_time: "ás" #eg. from 18:00 to 21:00
|
||||
to_time: "ás" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "Opções de restrição"
|
||||
restrict_with_labels: "Restrinja este slot com etiquetas"
|
||||
restrict_for_subscriptions: "Restringir este slot para os usuários da assinatura"
|
||||
@ -508,8 +545,8 @@ pt:
|
||||
on_DATE: "No {DATE}"
|
||||
from_DATE: "Em {DATE}"
|
||||
from_TIME: "Ás {TIME}"
|
||||
to_date: "ás" #eg: from 01/01 to 01/05
|
||||
to_time: "ás" #eg. from 18:00 to 21:00
|
||||
to_date: "ás" #e.g.: from 01/01 to 01/05
|
||||
to_time: "ás" #e.g. from 18:00 to 21:00
|
||||
title: "Título"
|
||||
dates: "Datas"
|
||||
booking: "Reserva"
|
||||
@ -1453,8 +1490,8 @@ pt:
|
||||
statistics: "Estatísticas"
|
||||
evolution: "Evolução"
|
||||
age_filter: "Filtro de idade"
|
||||
from_age: "Dos" #eg. from 8 to 40 years old
|
||||
to_age: "aos" #eg. from 8 to 40 years old
|
||||
from_age: "Dos" #e.g. from 8 to 40 years old
|
||||
to_age: "aos" #e.g. from 8 to 40 years old
|
||||
start: "Início:"
|
||||
end: "Fim:"
|
||||
custom_filter: "Filtro customizado"
|
||||
@ -2215,8 +2252,6 @@ pt:
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
all_products: "All products"
|
||||
create_a_product: "Create a product"
|
||||
successfully_deleted: "The product has been successfully deleted"
|
||||
unable_to_delete: "Unable to delete the product: "
|
||||
filter: "Filter"
|
||||
filter_clear: "Clear all"
|
||||
filter_apply: "Apply"
|
||||
@ -2238,6 +2273,7 @@ pt:
|
||||
sort: "Sort:"
|
||||
visible_only: "Visible products only"
|
||||
product_item:
|
||||
product: "product"
|
||||
visible: "visible"
|
||||
hidden: "hidden"
|
||||
stock:
|
||||
@ -2294,6 +2330,7 @@ pt:
|
||||
stocks: "Stock:"
|
||||
internal: "Private stock"
|
||||
external: "Public stock"
|
||||
edit: "Edit"
|
||||
all: "All types"
|
||||
remaining_stock: "Remaining stock"
|
||||
type_in: "Add"
|
||||
|
@ -2,8 +2,8 @@ zu:
|
||||
app:
|
||||
admin:
|
||||
edit_destroy_buttons:
|
||||
deleted: "crwdns36793:0{TYPE}crwdne36793:0"
|
||||
unable_to_delete: "crwdns36795:0{TYPE}crwdne36795:0"
|
||||
deleted: "crwdns36793:0crwdne36793:0"
|
||||
unable_to_delete: "crwdns36795:0crwdne36795:0"
|
||||
delete_item: "crwdns36797:0{TYPE}crwdne36797:0"
|
||||
confirm_delete: "crwdns36799:0crwdne36799:0"
|
||||
delete_confirmation: "crwdns36801:0{TYPE}crwdne36801:0"
|
||||
@ -152,12 +152,19 @@ zu:
|
||||
every_month: "crwdns31885:0crwdne31885:0"
|
||||
every_year: "crwdns31887:0crwdne31887:0"
|
||||
plan_form:
|
||||
general_information: "crwdns31889:0crwdne31889:0"
|
||||
ACTION_title: "crwdns37397:0ACTION={ACTION}crwdne37397:0"
|
||||
tab_settings: "crwdns37399:0crwdne37399:0"
|
||||
tab_usage_limits: "crwdns37401:0crwdne37401:0"
|
||||
description: "crwdns31931:0crwdne31931:0"
|
||||
general_settings: "crwdns37403:0crwdne37403:0"
|
||||
general_settings_info: "crwdns37405:0crwdne37405:0"
|
||||
activation_and_payment: "crwdns37407:0crwdne37407:0"
|
||||
name: "crwdns31891:0crwdne31891:0"
|
||||
name_max_length: "crwdns31893:0crwdne31893:0"
|
||||
group: "crwdns31895:0crwdne31895:0"
|
||||
transversal: "crwdns31897:0crwdne31897:0"
|
||||
transversal_help: "crwdns31899:0crwdne31899:0"
|
||||
display: "crwdns37409:0crwdne37409:0"
|
||||
category: "crwdns31901:0crwdne31901:0"
|
||||
category_help: "crwdns31903:0crwdne31903:0"
|
||||
number_of_periods: "crwdns31905:0crwdne31905:0"
|
||||
@ -173,7 +180,6 @@ zu:
|
||||
rolling_subscription_help: "crwdns31925:0crwdne31925:0"
|
||||
monthly_payment: "crwdns31927:0crwdne31927:0"
|
||||
monthly_payment_help: "crwdns31929:0crwdne31929:0"
|
||||
description: "crwdns31931:0crwdne31931:0"
|
||||
information_sheet: "crwdns31933:0crwdne31933:0"
|
||||
notified_partner: "crwdns31935:0crwdne31935:0"
|
||||
new_user: "crwdns31937:0crwdne31937:0"
|
||||
@ -185,9 +191,39 @@ zu:
|
||||
partner_plan: "crwdns31949:0crwdne31949:0"
|
||||
partner_plan_help: "crwdns31951:0crwdne31951:0"
|
||||
partner_created: "crwdns31953:0crwdne31953:0"
|
||||
ACTION_plan: "crwdns31955:0ACTION={ACTION}crwdne31955:0"
|
||||
slots_visibility: "crwdns37491:0crwdne37491:0"
|
||||
slots_visibility_help: "crwdns37493:0crwdne37493:0"
|
||||
machines_visibility: "crwdns37495:0crwdne37495:0"
|
||||
save: "crwdns37411:0crwdne37411:0"
|
||||
create_success: "crwdns31957:0crwdne31957:0"
|
||||
update_success: "crwdns31959:0crwdne31959:0"
|
||||
plan_limit_form:
|
||||
usage_limitation: "crwdns37413:0crwdne37413:0"
|
||||
usage_limitation_info: "crwdns37415:0crwdne37415:0"
|
||||
usage_limitation_switch: "crwdns37417:0crwdne37417:0"
|
||||
new_usage_limitation: "crwdns37419:0crwdne37419:0"
|
||||
all_limitations: "crwdns37421:0crwdne37421:0"
|
||||
by_category: "crwdns37477:0crwdne37477:0"
|
||||
by_machine: "crwdns37425:0crwdne37425:0"
|
||||
category: "crwdns37427:0crwdne37427:0"
|
||||
machine: "crwdns37429:0crwdne37429:0"
|
||||
max_hours_per_day: "crwdns37431:0crwdne37431:0"
|
||||
ongoing_limitations: "crwdns37433:0crwdne37433:0"
|
||||
saved_limitations: "crwdns37435:0crwdne37435:0"
|
||||
cancel: "crwdns37437:0crwdne37437:0"
|
||||
cancel_deletion: "crwdns37487:0crwdne37487:0"
|
||||
ongoing_deletion: "crwdns37489:0crwdne37489:0"
|
||||
plan_limit_modal:
|
||||
title: "crwdns37445:0crwdne37445:0"
|
||||
limit_reservations: "crwdns37447:0crwdne37447:0"
|
||||
by_category: "crwdns37479:0crwdne37479:0"
|
||||
by_machine: "crwdns37451:0crwdne37451:0"
|
||||
category: "crwdns37453:0crwdne37453:0"
|
||||
machine: "crwdns37455:0crwdne37455:0"
|
||||
categories_info: "crwdns37457:0crwdne37457:0"
|
||||
machine_info: "crwdns37459:0crwdne37459:0"
|
||||
max_hours_per_day: "crwdns37461:0crwdne37461:0"
|
||||
confirm: "crwdns37463:0crwdne37463:0"
|
||||
partner_modal:
|
||||
title: "crwdns31961:0crwdne31961:0"
|
||||
create_partner: "crwdns31963:0crwdne31963:0"
|
||||
@ -196,6 +232,7 @@ zu:
|
||||
email: "crwdns31969:0crwdne31969:0"
|
||||
plan_pricing_form:
|
||||
prices: "crwdns31971:0crwdne31971:0"
|
||||
about_prices: "crwdns37465:0crwdne37465:0"
|
||||
copy_prices_from: "crwdns31973:0crwdne31973:0"
|
||||
copy_prices_from_help: "crwdns31975:0crwdne31975:0"
|
||||
machines: "crwdns31977:0crwdne31977:0"
|
||||
@ -286,7 +323,7 @@ zu:
|
||||
manage_trainings: "crwdns24132:0crwdne24132:0"
|
||||
number_of_tickets: "crwdns24134:0crwdne24134:0"
|
||||
adjust_the_opening_hours: "crwdns24136:0crwdne24136:0"
|
||||
to_time: "crwdns24138:0crwdne24138:0" #eg. from 18:00 to 21:00
|
||||
to_time: "crwdns24138:0crwdne24138:0" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "crwdns24140:0crwdne24140:0"
|
||||
restrict_with_labels: "crwdns24142:0crwdne24142:0"
|
||||
restrict_for_subscriptions: "crwdns24144:0crwdne24144:0"
|
||||
@ -508,8 +545,8 @@ zu:
|
||||
on_DATE: "crwdns24452:0{DATE}crwdne24452:0"
|
||||
from_DATE: "crwdns24454:0{DATE}crwdne24454:0"
|
||||
from_TIME: "crwdns24456:0{TIME}crwdne24456:0"
|
||||
to_date: "crwdns24458:0crwdne24458:0" #eg: from 01/01 to 01/05
|
||||
to_time: "crwdns24460:0crwdne24460:0" #eg. from 18:00 to 21:00
|
||||
to_date: "crwdns24458:0crwdne24458:0" #e.g.: from 01/01 to 01/05
|
||||
to_time: "crwdns24460:0crwdne24460:0" #e.g. from 18:00 to 21:00
|
||||
title: "crwdns24462:0crwdne24462:0"
|
||||
dates: "crwdns24464:0crwdne24464:0"
|
||||
booking: "crwdns24466:0crwdne24466:0"
|
||||
@ -1453,8 +1490,8 @@ zu:
|
||||
statistics: "crwdns26224:0crwdne26224:0"
|
||||
evolution: "crwdns26226:0crwdne26226:0"
|
||||
age_filter: "crwdns26228:0crwdne26228:0"
|
||||
from_age: "crwdns26230:0crwdne26230:0" #eg. from 8 to 40 years old
|
||||
to_age: "crwdns26232:0crwdne26232:0" #eg. from 8 to 40 years old
|
||||
from_age: "crwdns26230:0crwdne26230:0" #e.g. from 8 to 40 years old
|
||||
to_age: "crwdns26232:0crwdne26232:0" #e.g. from 8 to 40 years old
|
||||
start: "crwdns26234:0crwdne26234:0"
|
||||
end: "crwdns26236:0crwdne26236:0"
|
||||
custom_filter: "crwdns26238:0crwdne26238:0"
|
||||
@ -2215,8 +2252,6 @@ zu:
|
||||
unexpected_error_occurred: "crwdns31338:0crwdne31338:0"
|
||||
all_products: "crwdns31340:0crwdne31340:0"
|
||||
create_a_product: "crwdns31342:0crwdne31342:0"
|
||||
successfully_deleted: "crwdns31344:0crwdne31344:0"
|
||||
unable_to_delete: "crwdns31346:0crwdne31346:0"
|
||||
filter: "crwdns31348:0crwdne31348:0"
|
||||
filter_clear: "crwdns31350:0crwdne31350:0"
|
||||
filter_apply: "crwdns31352:0crwdne31352:0"
|
||||
@ -2238,6 +2273,7 @@ zu:
|
||||
sort: "crwdns31380:0crwdne31380:0"
|
||||
visible_only: "crwdns31382:0crwdne31382:0"
|
||||
product_item:
|
||||
product: "crwdns37467:0crwdne37467:0"
|
||||
visible: "crwdns31384:0crwdne31384:0"
|
||||
hidden: "crwdns31386:0crwdne31386:0"
|
||||
stock:
|
||||
@ -2294,6 +2330,7 @@ zu:
|
||||
stocks: "crwdns31480:0crwdne31480:0"
|
||||
internal: "crwdns31482:0crwdne31482:0"
|
||||
external: "crwdns31484:0crwdne31484:0"
|
||||
edit: "crwdns37469:0crwdne37469:0"
|
||||
all: "crwdns31486:0crwdne31486:0"
|
||||
remaining_stock: "crwdns31488:0crwdne31488:0"
|
||||
type_in: "crwdns31490:0crwdne31490:0"
|
||||
|
@ -540,3 +540,6 @@ de:
|
||||
show_reserved_uniq: "Show only slots with reservations"
|
||||
machine:
|
||||
machine_uncategorized: "Uncategorized machines"
|
||||
form_unsaved_list:
|
||||
save_reminder: "Do not forget to save your changes"
|
||||
cancel: "Cancel"
|
||||
|
@ -540,3 +540,6 @@ en:
|
||||
show_reserved_uniq: "Show only slots with reservations"
|
||||
machine:
|
||||
machine_uncategorized: "Uncategorized machines"
|
||||
form_unsaved_list:
|
||||
save_reminder: "Do not forget to save your changes"
|
||||
cancel: "Cancel"
|
||||
|
@ -540,3 +540,6 @@ es:
|
||||
show_reserved_uniq: "Show only slots with reservations"
|
||||
machine:
|
||||
machine_uncategorized: "Uncategorized machines"
|
||||
form_unsaved_list:
|
||||
save_reminder: "Do not forget to save your changes"
|
||||
cancel: "Cancel"
|
||||
|
@ -540,3 +540,6 @@ fr:
|
||||
show_reserved_uniq: "Afficher uniquement les créneaux avec réservation"
|
||||
machine:
|
||||
machine_uncategorized: "Machines non classés"
|
||||
form_unsaved_list:
|
||||
save_reminder: "N'oubliez pas d'enregistrer vos modifications"
|
||||
cancel: "Annuler"
|
||||
|
@ -540,3 +540,6 @@
|
||||
show_reserved_uniq: "Show only slots with reservations"
|
||||
machine:
|
||||
machine_uncategorized: "Uncategorized machines"
|
||||
form_unsaved_list:
|
||||
save_reminder: "Do not forget to save your changes"
|
||||
cancel: "Cancel"
|
||||
|
@ -540,3 +540,6 @@ pt:
|
||||
show_reserved_uniq: "Show only slots with reservations"
|
||||
machine:
|
||||
machine_uncategorized: "Uncategorized machines"
|
||||
form_unsaved_list:
|
||||
save_reminder: "Do not forget to save your changes"
|
||||
cancel: "Cancel"
|
||||
|
@ -540,3 +540,6 @@ zu:
|
||||
show_reserved_uniq: "crwdns36249:0crwdne36249:0"
|
||||
machine:
|
||||
machine_uncategorized: "crwdns36219:0crwdne36219:0"
|
||||
form_unsaved_list:
|
||||
save_reminder: "crwdns37471:0crwdne37471:0"
|
||||
cancel: "crwdns37473:0crwdne37473:0"
|
||||
|
@ -433,6 +433,8 @@ de:
|
||||
schedule_deadline: "Sie müssen den Scheck zur %{DATE} -Frist einlösen, für den Zeitplan %{REFERENCE}"
|
||||
notify_admin_payment_schedule_transfer_deadline:
|
||||
schedule_deadline: "Sie müssen das Lastschriftverfahren für die %{DATE} -Frist bestätigen, für Zeitplan %{REFERENCE}"
|
||||
notify_member_reservation_limit_reached:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
|
||||
notify_admin_user_supporting_document_files_updated:
|
||||
@ -519,6 +521,7 @@ de:
|
||||
availability: "The availaility doesn't exist"
|
||||
full: "The slot is already fully reserved"
|
||||
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start"
|
||||
limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription"
|
||||
restricted: "This availability is restricted for subscribers"
|
||||
plan: "This subscription plan is disabled"
|
||||
plan_group: "This subscription plan is reserved for members of group %{GROUP}"
|
||||
|
@ -433,6 +433,8 @@ en:
|
||||
schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
notify_admin_payment_schedule_transfer_deadline:
|
||||
schedule_deadline: "You must confirm the bank direct debit for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
notify_member_reservation_limit_reached:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
|
||||
notify_admin_user_supporting_document_files_updated:
|
||||
@ -519,6 +521,7 @@ en:
|
||||
availability: "The availaility doesn't exist"
|
||||
full: "The slot is already fully reserved"
|
||||
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start"
|
||||
limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription"
|
||||
restricted: "This availability is restricted for subscribers"
|
||||
plan: "This subscription plan is disabled"
|
||||
plan_group: "This subscription plan is reserved for members of group %{GROUP}"
|
||||
|
@ -433,6 +433,8 @@ es:
|
||||
schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
notify_admin_payment_schedule_transfer_deadline:
|
||||
schedule_deadline: "You must confirm the bank direct debit for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
notify_member_reservation_limit_reached:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
|
||||
notify_admin_user_supporting_document_files_updated:
|
||||
@ -519,6 +521,7 @@ es:
|
||||
availability: "The availaility doesn't exist"
|
||||
full: "The slot is already fully reserved"
|
||||
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start"
|
||||
limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription"
|
||||
restricted: "This availability is restricted for subscribers"
|
||||
plan: "This subscription plan is disabled"
|
||||
plan_group: "This subscription plan is reserved for members of group %{GROUP}"
|
||||
|
@ -433,6 +433,8 @@ fr:
|
||||
schedule_deadline: "Vous devez encaisser le chèque de l'échéance du %{DATE}, pour l'échéancier %{REFERENCE}"
|
||||
notify_admin_payment_schedule_transfer_deadline:
|
||||
schedule_deadline: "Vous devez confirmer le prélèvement bancaire pour l'échéance du %{DATE} , pour l'échéancier %{REFERENCE}"
|
||||
notify_member_reservation_limit_reached:
|
||||
limit_reached: "Pour le %{DATE}, vous avez atteint votre limite quotidienne de %{HOURS} heures de réservation de la %{ITEM}."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
supporting_document_files_uploaded: "Le membre <strong><em>%{NAME}</strong></em> a téléversé un nouveau justificatif."
|
||||
notify_admin_user_supporting_document_files_updated:
|
||||
@ -519,6 +521,7 @@ fr:
|
||||
availability: "La disponibilité n'existe pas"
|
||||
full: "Le créneau est déjà entièrement réservé"
|
||||
deadline: "Vous ne pouvez pas réserver un créneau %{MINUTES} minutes avant son début"
|
||||
limit_reached: "Vous avez atteint la limite de réservation de %{HOURS}H par jour pour la %{RESERVABLE}, pour votre abonnement actuel"
|
||||
restricted: "Cette disponibilité n'est disponible que pour les abonnés"
|
||||
plan: "Cette formule d'abonnement est désactivé"
|
||||
plan_group: "Cette formule d'abonnement est réservée aux membres du groupe %{GROUP}"
|
||||
|
@ -375,6 +375,10 @@ de:
|
||||
remember: "Gemäß Ihrem Zahlungsplan von %{REFERENCE} wurde zum %{DATE} eine Belastung der Karte in Höhe von %{AMOUNT} geplant."
|
||||
date: "Dies ist eine Erinnerung zur Prüfung, ob das Bankkonto erfolgreich belastet werden konnte."
|
||||
confirm: "Bitte bestätigen Sie den Erhalt des Guthabens in Ihrer Zahlungsverwaltung, damit die entsprechende Rechnung generiert werden kann."
|
||||
notify_member_reservation_limit_reached:
|
||||
subject: "Daily reservation limit reached"
|
||||
body:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
subject: "Supporting documents uploaded by a member"
|
||||
body:
|
||||
|
@ -375,6 +375,10 @@ en:
|
||||
remember: "In accordance with your %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}."
|
||||
date: "This is a reminder to verify that the direct bank debit was successfull."
|
||||
confirm: "Please confirm the receipt of funds in your payment schedule management interface, so that the corresponding invoice will be generated."
|
||||
notify_member_reservation_limit_reached:
|
||||
subject: "Daily reservation limit reached"
|
||||
body:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
subject: "Supporting documents uploaded by a member"
|
||||
body:
|
||||
|
@ -375,6 +375,10 @@ es:
|
||||
remember: "In accordance with your %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}."
|
||||
date: "This is a reminder to verify that the direct bank debit was successfull."
|
||||
confirm: "Please confirm the receipt of funds in your payment schedule management interface, so that the corresponding invoice will be generated."
|
||||
notify_member_reservation_limit_reached:
|
||||
subject: "Daily reservation limit reached"
|
||||
body:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
subject: "Supporting documents uploaded by a member"
|
||||
body:
|
||||
|
@ -375,6 +375,10 @@ fr:
|
||||
remember: "Conformément à l'échéancier de paiement %{REFERENCE}, une échéance de %{AMOUNT} était prévu pour être prélevée le %{DATE}."
|
||||
date: "Ceci est un rappel pour vérifier que le prélèvement bancaire a bien été effectué."
|
||||
confirm: "Veuillez confirmer la réception des fonds dans votre interface de gestion des échéanciers de paiement, afin que la facture correspondante soit générée."
|
||||
notify_member_reservation_limit_reached:
|
||||
subject: "Limite de réservation quotidienne atteinte"
|
||||
body:
|
||||
limit_reached: "Pour le %{DATE}, vous avez atteint votre limite quotidienne de %{HOURS} heures de réservation de la %{ITEM}."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
subject: "Justificatif téléversé par un membre"
|
||||
body:
|
||||
|
@ -375,6 +375,10 @@
|
||||
remember: "In accordance with your %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}."
|
||||
date: "This is a reminder to verify that the direct bank debit was successfull."
|
||||
confirm: "Please confirm the receipt of funds in your payment schedule management interface, so that the corresponding invoice will be generated."
|
||||
notify_member_reservation_limit_reached:
|
||||
subject: "Daily reservation limit reached"
|
||||
body:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
subject: "Supporting documents uploaded by a member"
|
||||
body:
|
||||
|
@ -375,6 +375,10 @@ pt:
|
||||
remember: "De acordo com a agenda de pagamento %{REFERENCE}, %{AMOUNT} deveria ser debitado em %{DATE}."
|
||||
date: "Este é um lembrete para verificar se o débito bancário foi bem sucedido."
|
||||
confirm: "Não se esqueça de confirmar o recibo na interface de gestão de pagamento, para que a fatura correspondente seja gerada."
|
||||
notify_member_reservation_limit_reached:
|
||||
subject: "Daily reservation limit reached"
|
||||
body:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
subject: "Supporting documents uploaded by a member"
|
||||
body:
|
||||
|
@ -375,6 +375,10 @@ zu:
|
||||
remember: "crwdns29938:0%{REFERENCE}crwdnd29938:0%{AMOUNT}crwdnd29938:0%{DATE}crwdne29938:0"
|
||||
date: "crwdns29940:0crwdne29940:0"
|
||||
confirm: "crwdns29942:0crwdne29942:0"
|
||||
notify_member_reservation_limit_reached:
|
||||
subject: "crwdns37483:0crwdne37483:0"
|
||||
body:
|
||||
limit_reached: "crwdns37485:0%{DATE}crwdnd37485:0%{HOURS}crwdnd37485:0%{ITEM}crwdne37485:0"
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
subject: "crwdns37349:0crwdne37349:0"
|
||||
body:
|
||||
|
@ -433,6 +433,8 @@
|
||||
schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
notify_admin_payment_schedule_transfer_deadline:
|
||||
schedule_deadline: "You must confirm the bank direct debit for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
notify_member_reservation_limit_reached:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
|
||||
notify_admin_user_supporting_document_files_updated:
|
||||
@ -519,6 +521,7 @@
|
||||
availability: "The availaility doesn't exist"
|
||||
full: "The slot is already fully reserved"
|
||||
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start"
|
||||
limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription"
|
||||
restricted: "This availability is restricted for subscribers"
|
||||
plan: "This subscription plan is disabled"
|
||||
plan_group: "This subscription plan is reserved for members of group %{GROUP}"
|
||||
|
@ -433,6 +433,8 @@ pt:
|
||||
schedule_deadline: "Você deve realizar a verificação para a data limite de %{DATE} para agendar %{REFERENCE}"
|
||||
notify_admin_payment_schedule_transfer_deadline:
|
||||
schedule_deadline: "Você deve realizar a verificação do débito para a data limite de %{DATE}, para o agendamento %{REFERENCE}"
|
||||
notify_member_reservation_limit_reached:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
|
||||
notify_admin_user_supporting_document_files_updated:
|
||||
@ -519,6 +521,7 @@ pt:
|
||||
availability: "The availaility doesn't exist"
|
||||
full: "The slot is already fully reserved"
|
||||
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start"
|
||||
limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription"
|
||||
restricted: "This availability is restricted for subscribers"
|
||||
plan: "This subscription plan is disabled"
|
||||
plan_group: "This subscription plan is reserved for members of group %{GROUP}"
|
||||
|
@ -433,6 +433,8 @@ zu:
|
||||
schedule_deadline: "crwdns21120:0%{DATE}crwdnd21120:0%{REFERENCE}crwdne21120:0"
|
||||
notify_admin_payment_schedule_transfer_deadline:
|
||||
schedule_deadline: "crwdns22305:0%{DATE}crwdnd22305:0%{REFERENCE}crwdne22305:0"
|
||||
notify_member_reservation_limit_reached:
|
||||
limit_reached: "crwdns37481:0%{DATE}crwdnd37481:0%{HOURS}crwdnd37481:0%{ITEM}crwdne37481:0"
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
supporting_document_files_uploaded: "crwdns37341:0%{NAME}crwdne37341:0"
|
||||
notify_admin_user_supporting_document_files_updated:
|
||||
@ -519,6 +521,7 @@ zu:
|
||||
availability: "crwdns36269:0crwdne36269:0"
|
||||
full: "crwdns36271:0crwdne36271:0"
|
||||
deadline: "crwdns36273:0%{MINUTES}crwdne36273:0"
|
||||
limit_reached: "crwdns37475:0%{HOURS}crwdnd37475:0%{RESERVABLE}crwdne37475:0"
|
||||
restricted: "crwdns36275:0crwdne36275:0"
|
||||
plan: "crwdns36277:0crwdne36277:0"
|
||||
plan_group: "crwdns37207:0%{GROUP}crwdne37207:0"
|
||||
|
8
db/migrate/20230307123611_add_limiting_to_plan.rb
Normal file
8
db/migrate/20230307123611_add_limiting_to_plan.rb
Normal file
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# From this migration, any subscription plan can define restrictions on the reservation of resources
|
||||
class AddLimitingToPlan < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :plans, :limiting, :boolean
|
||||
end
|
||||
end
|
16
db/migrate/20230307123841_create_plan_limitations.rb
Normal file
16
db/migrate/20230307123841_create_plan_limitations.rb
Normal file
@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This table saves the restrictions settings, per plan and resource
|
||||
class CreatePlanLimitations < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :plan_limitations do |t|
|
||||
t.references :plan, foreign_key: true, index: true, null: false
|
||||
t.references :limitable, polymorphic: true, null: false
|
||||
t.integer :limit, null: false, default: 0
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :plan_limitations, %i[plan_id limitable_id limitable_type], unique: true, name: 'index_plan_limitations_on_plan_and_limitable'
|
||||
end
|
||||
end
|
@ -0,0 +1,9 @@
|
||||
# frozen_string_literal:true
|
||||
|
||||
# From this migration, we add a machines_visibility parameter to plans.
|
||||
# This parameter determines how far in advance subscribers can view and reserve machine slots.
|
||||
class AddMachineVisibilityToPlan < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :plans, :machines_visibility, :integer
|
||||
end
|
||||
end
|
31
db/schema.rb
31
db/schema.rb
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
ActiveRecord::Schema.define(version: 2023_03_15_095054) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "fuzzystrmatch"
|
||||
@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
enable_extension "unaccent"
|
||||
|
||||
create_table "abuses", id: :serial, force: :cascade do |t|
|
||||
t.integer "signaled_id"
|
||||
t.string "signaled_type"
|
||||
t.integer "signaled_id"
|
||||
t.string "first_name"
|
||||
t.string "last_name"
|
||||
t.string "email"
|
||||
@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
t.string "locality"
|
||||
t.string "country"
|
||||
t.string "postal_code"
|
||||
t.integer "placeable_id"
|
||||
t.string "placeable_type"
|
||||
t.integer "placeable_id"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
end
|
||||
|
||||
create_table "assets", id: :serial, force: :cascade do |t|
|
||||
t.integer "viewable_id"
|
||||
t.string "viewable_type"
|
||||
t.integer "viewable_id"
|
||||
t.string "attachment"
|
||||
t.string "type"
|
||||
t.datetime "created_at"
|
||||
@ -164,10 +164,10 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
|
||||
create_table "cart_item_event_reservation_tickets", force: :cascade do |t|
|
||||
t.integer "booked"
|
||||
t.bigint "event_price_category_id"
|
||||
t.bigint "cart_item_event_reservation_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.bigint "event_price_category_id"
|
||||
t.index ["cart_item_event_reservation_id"], name: "index_cart_item_tickets_on_cart_item_event_reservation"
|
||||
t.index ["event_price_category_id"], name: "index_cart_item_tickets_on_event_price_category"
|
||||
end
|
||||
@ -282,8 +282,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
end
|
||||
|
||||
create_table "credits", id: :serial, force: :cascade do |t|
|
||||
t.integer "creditable_id"
|
||||
t.string "creditable_type"
|
||||
t.integer "creditable_id"
|
||||
t.integer "plan_id"
|
||||
t.integer "hours"
|
||||
t.datetime "created_at"
|
||||
@ -546,15 +546,15 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
|
||||
create_table "notifications", id: :serial, force: :cascade do |t|
|
||||
t.integer "receiver_id"
|
||||
t.integer "attached_object_id"
|
||||
t.string "attached_object_type"
|
||||
t.integer "attached_object_id"
|
||||
t.integer "notification_type_id"
|
||||
t.boolean "is_read", default: false
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.string "receiver_type"
|
||||
t.boolean "is_send", default: false
|
||||
t.jsonb "meta_data", default: {}
|
||||
t.jsonb "meta_data", default: "{}"
|
||||
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
|
||||
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
||||
end
|
||||
@ -740,13 +740,14 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
end
|
||||
|
||||
create_table "plan_limitations", force: :cascade do |t|
|
||||
t.bigint "plan_id"
|
||||
t.string "limitable_type"
|
||||
t.bigint "limitable_id"
|
||||
t.bigint "plan_id", null: false
|
||||
t.string "limitable_type", null: false
|
||||
t.bigint "limitable_id", null: false
|
||||
t.integer "limit", default: 0, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["limitable_type", "limitable_id"], name: "index_plan_limitations_on_limitable_type_and_limitable_id"
|
||||
t.index ["plan_id", "limitable_id", "limitable_type"], name: "index_plan_limitations_on_plan_and_limitable", unique: true
|
||||
t.index ["plan_id"], name: "index_plan_limitations_on_plan_id"
|
||||
end
|
||||
|
||||
@ -770,6 +771,7 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
t.boolean "monthly_payment"
|
||||
t.bigint "plan_category_id"
|
||||
t.boolean "limiting"
|
||||
t.integer "machines_visibility"
|
||||
t.index ["group_id"], name: "index_plans_on_group_id"
|
||||
t.index ["plan_category_id"], name: "index_plans_on_plan_category_id"
|
||||
end
|
||||
@ -811,15 +813,14 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
t.text "conditions"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index "btrim(lower((name)::text))", name: "index_price_categories_on_TRIM_BOTH_FROM_LOWER_name", unique: true
|
||||
t.index ["name"], name: "index_price_categories_on_name", unique: true
|
||||
end
|
||||
|
||||
create_table "prices", id: :serial, force: :cascade do |t|
|
||||
t.integer "group_id"
|
||||
t.integer "plan_id"
|
||||
t.integer "priceable_id"
|
||||
t.string "priceable_type"
|
||||
t.integer "priceable_id"
|
||||
t.integer "amount"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
@ -983,8 +984,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
t.text "message"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.integer "reservable_id"
|
||||
t.string "reservable_type"
|
||||
t.integer "reservable_id"
|
||||
t.integer "nb_reserve_places"
|
||||
t.integer "statistic_profile_id"
|
||||
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
|
||||
@ -993,8 +994,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
|
||||
create_table "roles", id: :serial, force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.integer "resource_id"
|
||||
t.string "resource_type"
|
||||
t.integer "resource_id"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
|
||||
|
@ -23,3 +23,11 @@ unless NotificationType.find_by(name: 'notify_admin_order_is_paid')
|
||||
is_configurable: true
|
||||
)
|
||||
end
|
||||
|
||||
unless NotificationType.find_by(name: 'notify_member_reservation_limit_reached')
|
||||
NotificationType.create!(
|
||||
name: 'notify_member_reservation_limit_reached',
|
||||
category: 'agenda',
|
||||
is_configurable: false
|
||||
)
|
||||
end
|
||||
|
@ -161,7 +161,7 @@
|
||||
"react-cool-onclickoutside": "^1.7.0",
|
||||
"react-custom-events": "^1.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hook-form": "^7.30.0",
|
||||
"react-hook-form": "~7.31.3",
|
||||
"react-i18next": "^11.15.6",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-select": "^5.3.2",
|
||||
|
@ -34,7 +34,7 @@ list_files() {
|
||||
|
||||
update_file() {
|
||||
# params: FILE_ID, STORAGE_ID
|
||||
curl -s -X PUT "https://api.crowdin.com/api/v2/projects/$PROJECT_ID/files/$1" -H "$(authorization)" -H "Content-Type: application/json" -d "{ \"storageId\": $2, \"updateOption\": \"keep_translations_and_approvals\" }"
|
||||
curl -s -X PUT "https://api.crowdin.com/api/v2/projects/$PROJECT_ID/files/$1" -H "$(authorization)" -H "Content-Type: application/json" -d "{ \"storageId\": $2 }"
|
||||
}
|
||||
|
||||
find_file_id() {
|
||||
|
8
test/fixtures/users.yml
vendored
8
test/fixtures/users.yml
vendored
@ -1,3 +1,4 @@
|
||||
# admin without subscription
|
||||
user_1:
|
||||
id: 1
|
||||
username: admin
|
||||
@ -30,6 +31,7 @@ user_1:
|
||||
merged_at:
|
||||
is_allow_newsletter: true
|
||||
|
||||
# member without subscription
|
||||
user_2:
|
||||
id: 2
|
||||
username: jdupond
|
||||
@ -62,6 +64,7 @@ user_2:
|
||||
merged_at:
|
||||
is_allow_newsletter: true
|
||||
|
||||
# member with 1 month subscription (plan 2/standard)
|
||||
user_3:
|
||||
id: 3
|
||||
username: pdurand
|
||||
@ -94,6 +97,7 @@ user_3:
|
||||
merged_at:
|
||||
is_allow_newsletter: false
|
||||
|
||||
# member with 1 month subscription (plan 3/students)
|
||||
user_4:
|
||||
id: 4
|
||||
username: kdumas
|
||||
@ -126,6 +130,7 @@ user_4:
|
||||
merged_at:
|
||||
is_allow_newsletter: false
|
||||
|
||||
# member with 10€ on wallet
|
||||
user_5:
|
||||
id: 5
|
||||
username: vlonchamp
|
||||
@ -158,6 +163,7 @@ user_5:
|
||||
merged_at:
|
||||
is_allow_newsletter: true
|
||||
|
||||
# partner of plan 2
|
||||
user_6:
|
||||
id: 6
|
||||
username: GilbertPartenaire
|
||||
@ -190,6 +196,7 @@ user_6:
|
||||
merged_at:
|
||||
is_allow_newsletter: true
|
||||
|
||||
# member with 255€ on wallet
|
||||
user_7:
|
||||
id: 7
|
||||
username: lseguin
|
||||
@ -286,6 +293,7 @@ user_9:
|
||||
merged_at:
|
||||
is_allow_newsletter: true
|
||||
|
||||
# member with 1 year subscription
|
||||
user_10:
|
||||
id: 10
|
||||
username: acamus
|
||||
|
@ -7,6 +7,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import plans from '../../__fixtures__/plans';
|
||||
import machines from '../../__fixtures__/machines';
|
||||
import { tiptapEvent } from '../../__lib__/tiptap';
|
||||
import { uiRouter } from '../../__lib__/ui-router';
|
||||
|
||||
describe('PlanForm', () => {
|
||||
const onError = jest.fn();
|
||||
@ -14,7 +15,7 @@ describe('PlanForm', () => {
|
||||
const beforeSubmit = jest.fn();
|
||||
|
||||
test('render create PlanForm', async () => {
|
||||
render(<PlanForm action="create" onError={onError} onSuccess={onSuccess} />);
|
||||
render(<PlanForm action="create" onError={onError} onSuccess={onSuccess} uiRouter={uiRouter} />);
|
||||
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ }));
|
||||
expect(screen.getByLabelText(/app.admin.plan_form.name/)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/app.admin.plan_form.transversal/)).toBeInTheDocument();
|
||||
@ -31,11 +32,11 @@ describe('PlanForm', () => {
|
||||
expect(screen.getByLabelText(/app.admin.plan_form.period/)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/app.admin.plan_form.partner_plan/)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('plan-pricing-form')).toBeNull();
|
||||
expect(screen.getByRole('button', { name: /app.admin.plan_form.ACTION_plan/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /app.admin.plan_form.save/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('create new plan', async () => {
|
||||
render(<PlanForm action="create" onError={onError} onSuccess={onSuccess} beforeSubmit={beforeSubmit} />);
|
||||
render(<PlanForm action="create" onError={onError} onSuccess={onSuccess} beforeSubmit={beforeSubmit} uiRouter={uiRouter} />);
|
||||
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ }));
|
||||
const user = userEvent.setup();
|
||||
// base_name
|
||||
@ -66,7 +67,7 @@ describe('PlanForm', () => {
|
||||
// advanced_accounting_attributes.analytical_section
|
||||
fireEvent.change(screen.getByLabelText(/app.admin.advanced_accounting_form.analytical_section/), { target: { value: '9B20A' } });
|
||||
// send the form
|
||||
fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.ACTION_plan/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.save/ }));
|
||||
await waitFor(() => {
|
||||
const expected: Plan = {
|
||||
base_name: 'Test Plan',
|
||||
@ -98,7 +99,7 @@ describe('PlanForm', () => {
|
||||
|
||||
test('render update PlanForm with partner', async () => {
|
||||
const plan = plans[1];
|
||||
render(<PlanForm action="update" plan={plan} onError={onError} onSuccess={onSuccess} />);
|
||||
render(<PlanForm action="update" plan={plan} onError={onError} onSuccess={onSuccess} uiRouter={uiRouter} />);
|
||||
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_pricing_form.copy_prices_from/ }));
|
||||
expect(screen.getByLabelText(/app.admin.plan_form.name/)).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/app.admin.plan_form.transversal/)).toBeNull();
|
||||
@ -119,18 +120,18 @@ describe('PlanForm', () => {
|
||||
expect(screen.getByLabelText(/app.admin.plan_form.notified_partner/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/app.admin.plan_form.alert_partner_notification/)).toBeInTheDocument();
|
||||
expect(screen.getByTestId('plan-pricing-form')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /app.admin.plan_form.ACTION_plan/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /app.admin.plan_form.save/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('selecting transversal plan disables group select', async () => {
|
||||
render(<PlanForm action="create" onError={onError} onSuccess={onSuccess} />);
|
||||
render(<PlanForm action="create" onError={onError} onSuccess={onSuccess} uiRouter={uiRouter} />);
|
||||
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ }));
|
||||
fireEvent.click(screen.getByRole('switch', { name: /app.admin.plan_form.transversal/ }));
|
||||
expect(screen.queryByRole('combobox', { name: /app.admin.plan_form.group/ })).toBeNull();
|
||||
});
|
||||
|
||||
test('selecting partner plan shows partner selection', async () => {
|
||||
render(<PlanForm action="create" onError={onError} onSuccess={onSuccess} />);
|
||||
render(<PlanForm action="create" onError={onError} onSuccess={onSuccess} uiRouter={uiRouter} />);
|
||||
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ }));
|
||||
fireEvent.click(screen.getByRole('switch', { name: /app.admin.plan_form.partner_plan/ }));
|
||||
expect(screen.getByLabelText(/app.admin.plan_form.notified_partner/));
|
||||
@ -138,7 +139,7 @@ describe('PlanForm', () => {
|
||||
});
|
||||
|
||||
test('creating a new partner selects him by default', async () => {
|
||||
render(<PlanForm action="create" onError={onError} onSuccess={onSuccess} />);
|
||||
render(<PlanForm action="create" onError={onError} onSuccess={onSuccess} uiRouter={uiRouter} />);
|
||||
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ }));
|
||||
fireEvent.click(screen.getByRole('switch', { name: /app.admin.plan_form.partner_plan/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.new_user/ }));
|
||||
@ -157,12 +158,12 @@ describe('PlanForm', () => {
|
||||
test('update plan prices', async () => {
|
||||
const plan = plans[1];
|
||||
const machine = machines[1];
|
||||
render(<PlanForm action="update" plan={plan} onError={onError} onSuccess={onSuccess} beforeSubmit={beforeSubmit} />);
|
||||
render(<PlanForm action="update" plan={plan} onError={onError} onSuccess={onSuccess} beforeSubmit={beforeSubmit} uiRouter={uiRouter} />);
|
||||
await waitFor(() => screen.getByLabelText(new RegExp(machine.name)));
|
||||
// update machine price
|
||||
fireEvent.change(screen.getByLabelText(new RegExp(machine.name)), { target: { value: 42.42 } });
|
||||
// send the form
|
||||
fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.ACTION_plan/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.save/ }));
|
||||
await waitFor(() => {
|
||||
const expected = {
|
||||
prices_attributes: expect.arrayContaining([{
|
||||
|
@ -72,7 +72,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest
|
||||
}.to_json, headers: default_headers
|
||||
end
|
||||
|
||||
assert_equal 201, response.status
|
||||
assert_equal 201, response.status, response.body
|
||||
|
||||
assert_equal reservations_count + 1, Reservation.count
|
||||
assert_equal invoices_count + 1, Invoice.count
|
||||
@ -105,7 +105,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest
|
||||
}
|
||||
}
|
||||
|
||||
assert_equal 201, response.status
|
||||
assert_equal 201, response.status, response.body
|
||||
|
||||
# Check the id
|
||||
availability = json_response(response.body)
|
||||
@ -141,7 +141,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest
|
||||
}.to_json, headers: default_headers
|
||||
end
|
||||
|
||||
assert_equal 422, response.status
|
||||
assert_equal 422, response.status, response.body
|
||||
assert_match(/availability is restricted for subscribers/, response.body)
|
||||
|
||||
assert_equal reservations_count, Reservation.count
|
||||
@ -175,7 +175,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest
|
||||
}
|
||||
}
|
||||
|
||||
assert_equal 201, response.status
|
||||
assert_equal 201, response.status, response.body
|
||||
|
||||
# Check the id
|
||||
availability = json_response(response.body)
|
||||
@ -187,27 +187,25 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest
|
||||
slot = Availability.find(availability[:id]).slots.first
|
||||
|
||||
# book a reservation
|
||||
VCR.use_cassette('reservations_create_for_restricted_slot_forced') do
|
||||
post '/api/local_payment/confirm_payment',
|
||||
params: {
|
||||
customer_id: @jdupont.id,
|
||||
items: [
|
||||
{
|
||||
reservation: {
|
||||
reservable_id: 2,
|
||||
reservable_type: 'Machine',
|
||||
slots_reservations_attributes: [
|
||||
{
|
||||
slot_id: slot.id
|
||||
}
|
||||
]
|
||||
}
|
||||
post '/api/local_payment/confirm_payment',
|
||||
params: {
|
||||
customer_id: @jdupont.id,
|
||||
items: [
|
||||
{
|
||||
reservation: {
|
||||
reservable_id: 2,
|
||||
reservable_type: 'Machine',
|
||||
slots_reservations_attributes: [
|
||||
{
|
||||
slot_id: slot.id
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}.to_json, headers: default_headers
|
||||
end
|
||||
}
|
||||
]
|
||||
}.to_json, headers: default_headers
|
||||
|
||||
assert_equal 201, response.status
|
||||
assert_equal 201, response.status, response.body
|
||||
|
||||
# Check the result
|
||||
result = json_response(response.body)
|
||||
@ -246,7 +244,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest
|
||||
}
|
||||
}
|
||||
|
||||
assert_equal 201, response.status
|
||||
assert_equal 201, response.status, response.body
|
||||
|
||||
# Check the id
|
||||
availability = json_response(response.body)
|
||||
@ -287,7 +285,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest
|
||||
}.to_json, headers: default_headers
|
||||
end
|
||||
|
||||
assert_equal 201, response.status
|
||||
assert_equal 201, response.status, response.body
|
||||
|
||||
# Check the result
|
||||
result = json_response(response.body)
|
||||
|
@ -3,7 +3,7 @@
|
||||
require 'test_helper'
|
||||
|
||||
# Test the service returning the availabilities for the given resources
|
||||
class AvailabilitiesServiceTest < ActiveSupport::TestCase
|
||||
class Availabilities::AvailabilitiesServiceTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@no_subscription = User.find_by(username: 'jdupond')
|
||||
@with_subscription = User.find_by(username: 'kdumas')
|
130
test/services/availabilities/visibility_service_test.rb
Normal file
130
test/services/availabilities/visibility_service_test.rb
Normal file
@ -0,0 +1,130 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
# Test the service returning the visibility window for availabilities
|
||||
class Availabilities::VisibilityServiceTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@admin = User.find_by(username: 'admin')
|
||||
@no_subscription = User.find_by(username: 'jdupond')
|
||||
@with_subscription = User.find_by(username: 'kdumas')
|
||||
@with_1y_subscription = User.find_by(username: 'acamus')
|
||||
# from the fixtures:
|
||||
# - visibility_others = 1 month
|
||||
# - visibility_yearly = 3 months
|
||||
end
|
||||
|
||||
test 'admin visibility for the coming month' do
|
||||
starting = Time.current.beginning_of_day
|
||||
ending = 1.month.from_now.end_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@admin, 'space', starting, ending)
|
||||
assert_equal starting, window[0]
|
||||
assert_equal ending, window[1]
|
||||
end
|
||||
|
||||
test 'admin visibility for the previous month' do
|
||||
starting = 1.month.ago.end_of_day
|
||||
ending = Time.current.beginning_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@admin, 'space', starting, ending)
|
||||
assert_equal starting, window[0]
|
||||
assert_equal ending, window[1]
|
||||
end
|
||||
|
||||
test 'admin visibility for the coming year' do
|
||||
starting = Time.current.beginning_of_day
|
||||
ending = 1.year.from_now.end_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@admin, 'space', starting, ending)
|
||||
assert_equal starting, window[0]
|
||||
assert_equal ending, window[1]
|
||||
end
|
||||
|
||||
test 'member visibility for the coming month' do
|
||||
starting = Time.current.beginning_of_day
|
||||
ending = 1.month.from_now.end_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@no_subscription, 'space', starting, ending)
|
||||
assert_datetimes_equal Time.current, window[0]
|
||||
assert_datetimes_equal 1.month.from_now, window[1]
|
||||
end
|
||||
|
||||
test 'member visibility for the previous month' do
|
||||
starting = 1.month.ago.end_of_day
|
||||
ending = Time.current.beginning_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@no_subscription, 'space', starting, ending)
|
||||
assert_datetimes_equal Time.current, window[0]
|
||||
assert_equal ending, window[1]
|
||||
end
|
||||
|
||||
test 'member visibility for the coming year' do
|
||||
starting = Time.current.beginning_of_day
|
||||
ending = 1.year.from_now.end_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@no_subscription, 'space', starting, ending)
|
||||
assert_datetimes_equal Time.current, window[0]
|
||||
assert_datetimes_equal 1.month.from_now, window[1]
|
||||
end
|
||||
|
||||
test 'subscriber visibility for the coming month' do
|
||||
starting = Time.current.beginning_of_day
|
||||
ending = 1.month.from_now.end_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@with_subscription, 'space', starting, ending)
|
||||
assert_datetimes_equal Time.current, window[0]
|
||||
assert_datetimes_equal 1.month.from_now, window[1]
|
||||
end
|
||||
|
||||
test 'subscriber visibility for the previous month' do
|
||||
starting = 1.month.ago.end_of_day
|
||||
ending = Time.current.beginning_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@with_subscription, 'space', starting, ending)
|
||||
assert_datetimes_equal Time.current, window[0]
|
||||
assert_equal ending, window[1]
|
||||
end
|
||||
|
||||
test 'subscriber visibility for the coming year' do
|
||||
starting = Time.current.beginning_of_day
|
||||
ending = 1.year.from_now.end_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@with_subscription, 'space', starting, ending)
|
||||
assert_datetimes_equal Time.current, window[0]
|
||||
assert_datetimes_equal 1.month.from_now, window[1]
|
||||
end
|
||||
|
||||
test '1 year subscriber visibility for the coming month' do
|
||||
starting = Time.current.beginning_of_day
|
||||
ending = 1.month.from_now.end_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@with_1y_subscription, 'space', starting, ending)
|
||||
assert_datetimes_equal Time.current, window[0]
|
||||
assert_equal ending, window[1]
|
||||
end
|
||||
|
||||
test '1 year subscriber visibility for the previous month' do
|
||||
starting = 1.month.ago.end_of_day
|
||||
ending = Time.current.beginning_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@with_1y_subscription, 'space', starting, ending)
|
||||
assert_datetimes_equal Time.current, window[0]
|
||||
assert_equal ending, window[1]
|
||||
end
|
||||
|
||||
test '1 year subscriber visibility for the coming year' do
|
||||
starting = Time.current.beginning_of_day
|
||||
ending = 1.year.from_now.end_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@with_1y_subscription, 'space', starting, ending)
|
||||
assert_datetimes_equal Time.current, window[0]
|
||||
assert_datetimes_equal 3.months.from_now, window[1]
|
||||
end
|
||||
|
||||
test '1 year subscriber visibility for trainings in the coming year' do
|
||||
starting = Time.current.beginning_of_day
|
||||
ending = 1.year.from_now.end_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@with_1y_subscription, 'training', starting, ending)
|
||||
assert_datetimes_equal Time.current, window[0]
|
||||
assert_datetimes_equal 1.month.from_now, window[1]
|
||||
end
|
||||
|
||||
test 'subscriber with plan custom visibility' do
|
||||
plan = @with_subscription.subscribed_plan
|
||||
plan.update(machines_visibility: 48)
|
||||
starting = Time.current.beginning_of_day
|
||||
ending = 1.month.from_now.end_of_day
|
||||
window = Availabilities::VisibilityService.new.visibility(@with_subscription, 'machine', starting, ending)
|
||||
assert_datetimes_equal Time.current, window[0]
|
||||
assert_datetimes_equal 48.hours.from_now, window[1]
|
||||
end
|
||||
end
|
173
test/services/reservation_limit_service_test.rb
Normal file
173
test/services/reservation_limit_service_test.rb
Normal file
@ -0,0 +1,173 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ReservationLimitServiceTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@acamus = User.find_by(username: 'acamus')
|
||||
@admin = User.find_by(username: 'admin')
|
||||
@machine = Machine.first
|
||||
@plan = Plan.find(1)
|
||||
end
|
||||
|
||||
test 'simple reservation without plan' do
|
||||
reservation = CartItem::MachineReservation.new(
|
||||
customer_profile: @acamus.invoicing_profile,
|
||||
operator_profile: @acamus.invoicing_profile,
|
||||
reservable: @machine,
|
||||
cart_item_reservation_slots_attributes: [{ slot_id: @machine.availabilities.first.slots.first.id }]
|
||||
)
|
||||
assert ReservationLimitService.authorized?(nil, @acamus, reservation, [])
|
||||
end
|
||||
|
||||
test 'simple reservation with not limiting plan' do
|
||||
reservation = CartItem::MachineReservation.new(
|
||||
customer_profile: @acamus.invoicing_profile,
|
||||
operator_profile: @acamus.invoicing_profile,
|
||||
reservable: @machine,
|
||||
cart_item_reservation_slots_attributes: [{ slot_id: @machine.availabilities.first.slots.first.id }]
|
||||
)
|
||||
assert ReservationLimitService.authorized?(@plan, @acamus, reservation, [])
|
||||
end
|
||||
|
||||
test 'simple reservation with limiting plan' do
|
||||
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable_id: @machine.id, limitable_type: 'Machine', limit: 2 }])
|
||||
reservation = CartItem::MachineReservation.new(
|
||||
customer_profile: @acamus.invoicing_profile,
|
||||
operator_profile: @acamus.invoicing_profile,
|
||||
reservable: @machine,
|
||||
cart_item_reservation_slots_attributes: [{ slot_id: @machine.availabilities.first.slots.first.id }]
|
||||
)
|
||||
assert ReservationLimitService.authorized?(@plan, @acamus, reservation, [])
|
||||
end
|
||||
|
||||
test 'reservation exceeds plan limit' do
|
||||
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable_id: @machine.id, limitable_type: 'Machine', limit: 2 }])
|
||||
slots = Availabilities::AvailabilitiesService.new(@acamus)
|
||||
.machines([@machine], @acamus, { start: Time.current, end: 10.days.from_now })
|
||||
|
||||
reservation = CartItem::MachineReservation.new(
|
||||
customer_profile: @acamus.invoicing_profile,
|
||||
operator_profile: @acamus.invoicing_profile,
|
||||
reservable: @machine,
|
||||
cart_item_reservation_slots_attributes: [{ slot: slots[2] }, { slot: slots[3] }, { slot: slots[4] }]
|
||||
)
|
||||
assert_not ReservationLimitService.authorized?(@plan, @acamus, reservation, [])
|
||||
end
|
||||
|
||||
test 'second reservation at plan limit' do
|
||||
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable_id: @machine.id, limitable_type: 'Machine', limit: 2 }])
|
||||
slots = Availabilities::AvailabilitiesService.new(@acamus)
|
||||
.machines([@machine], @acamus, { start: Time.current, end: 10.days.from_now })
|
||||
|
||||
reservation = CartItem::MachineReservation.new(
|
||||
customer_profile: @acamus.invoicing_profile,
|
||||
operator_profile: @acamus.invoicing_profile,
|
||||
reservable: @machine,
|
||||
cart_item_reservation_slots_attributes: [{ slot: slots[0] }]
|
||||
)
|
||||
reservation2 = CartItem::MachineReservation.new(
|
||||
customer_profile: @acamus.invoicing_profile,
|
||||
operator_profile: @acamus.invoicing_profile,
|
||||
reservable: @machine,
|
||||
cart_item_reservation_slots_attributes: [{ slot: slots[1] }]
|
||||
)
|
||||
assert ReservationLimitService.authorized?(@plan, @acamus, reservation2, [reservation])
|
||||
end
|
||||
|
||||
test 'second reservation exceeds plan limit' do
|
||||
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable_id: @machine.id, limitable_type: 'Machine', limit: 2 }])
|
||||
slots = Availabilities::AvailabilitiesService.new(@acamus)
|
||||
.machines([@machine], @acamus, { start: Time.current, end: 10.days.from_now })
|
||||
|
||||
reservation = CartItem::MachineReservation.new(
|
||||
customer_profile: @acamus.invoicing_profile,
|
||||
operator_profile: @acamus.invoicing_profile,
|
||||
reservable: @machine,
|
||||
cart_item_reservation_slots_attributes: [{ slot: slots[0] }, { slot: slots[1] }]
|
||||
)
|
||||
reservation2 = CartItem::MachineReservation.new(
|
||||
customer_profile: @acamus.invoicing_profile,
|
||||
operator_profile: @acamus.invoicing_profile,
|
||||
reservable: @machine,
|
||||
cart_item_reservation_slots_attributes: [{ slot: slots[2] }]
|
||||
)
|
||||
assert_not ReservationLimitService.authorized?(@plan, @acamus, reservation2, [reservation])
|
||||
end
|
||||
|
||||
test 'reservation of other resource should not conflict' do
|
||||
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable_id: @machine.id, limitable_type: 'Machine', limit: 2 }])
|
||||
slots = Availabilities::AvailabilitiesService.new(@acamus)
|
||||
.machines([@machine], @acamus, { start: Time.current, end: 10.days.from_now })
|
||||
|
||||
reservation = CartItem::SpaceReservation.new(
|
||||
customer_profile: @acamus.invoicing_profile,
|
||||
operator_profile: @acamus.invoicing_profile,
|
||||
reservable: Space.first,
|
||||
cart_item_reservation_slots_attributes: [{ slot: Space.first.availabilities.first.slots.first },
|
||||
{ slot: Space.first.availabilities.first.slots.last }]
|
||||
)
|
||||
reservation2 = CartItem::MachineReservation.new(
|
||||
customer_profile: @acamus.invoicing_profile,
|
||||
operator_profile: @acamus.invoicing_profile,
|
||||
reservable: @machine,
|
||||
cart_item_reservation_slots_attributes: [{ slot: slots[0] }]
|
||||
)
|
||||
assert ReservationLimitService.authorized?(@plan, @acamus, reservation2, [reservation])
|
||||
end
|
||||
|
||||
test 'get plan limit' do
|
||||
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable_id: @machine.id, limitable_type: 'Machine', limit: 2 }])
|
||||
assert_equal 2, ReservationLimitService.limit(@plan, @machine).limit
|
||||
end
|
||||
|
||||
test 'get plan without limit' do
|
||||
assert_nil ReservationLimitService.limit(@plan, @machine)
|
||||
end
|
||||
|
||||
test 'get category limit' do
|
||||
category = MachineCategory.find(1)
|
||||
category.update(machine_ids: [@machine.id])
|
||||
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable: category, limit: 4 }])
|
||||
assert_equal 4, ReservationLimitService.limit(@plan, @machine).limit
|
||||
end
|
||||
|
||||
test 'machine limit should override the category limit' do
|
||||
category = MachineCategory.find(1)
|
||||
category.update(machine_ids: [@machine.id])
|
||||
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable: @machine, limit: 2 }, { limitable: category, limit: 4 }])
|
||||
limit = ReservationLimitService.limit(@plan, @machine)
|
||||
assert_equal 2, limit.limit
|
||||
assert_equal @machine, limit.limitable
|
||||
end
|
||||
|
||||
test 'reservation reaches the limit' do
|
||||
user = User.find_by(username: 'kdumas')
|
||||
plan = user.subscribed_plan
|
||||
plan.update(limiting: true, plan_limitations_attributes: [{ limitable: @machine, limit: 1 }])
|
||||
slots = Availabilities::AvailabilitiesService.new(user)
|
||||
.machines([@machine], user, { start: Time.current, end: 10.days.from_now })
|
||||
reservation = Reservation.create!(
|
||||
statistic_profile: user.statistic_profile,
|
||||
reservable: @machine,
|
||||
slots_reservations_attributes: [{ slot: slots.last }]
|
||||
)
|
||||
reservation.reload
|
||||
assert_equal slots.last.start_at.to_date, ReservationLimitService.reached_limit_date(reservation)
|
||||
end
|
||||
|
||||
test 'reservation does not reaches the limit' do
|
||||
user = User.find_by(username: 'kdumas')
|
||||
plan = user.subscribed_plan
|
||||
plan.update(limiting: true, plan_limitations_attributes: [{ limitable: @machine, limit: 2 }])
|
||||
slots = Availabilities::AvailabilitiesService.new(user)
|
||||
.machines([@machine], user, { start: Time.current, end: 10.days.from_now })
|
||||
reservation = Reservation.create!(
|
||||
statistic_profile: user.statistic_profile,
|
||||
reservable: @machine,
|
||||
slots_reservations_attributes: [{ slot: slots.last }]
|
||||
)
|
||||
reservation.reload
|
||||
assert_nil ReservationLimitService.reached_limit_date(reservation)
|
||||
end
|
||||
end
|
@ -111,6 +111,11 @@ class ActiveSupport::TestCase
|
||||
assert_not_nil actual, msg
|
||||
assert_equal expected.to_date, actual.to_date, msg
|
||||
end
|
||||
|
||||
def assert_datetimes_equal(expected, actual, msg = nil)
|
||||
assert_not_nil actual, msg
|
||||
assert_equal expected.iso8601, actual.iso8601, msg
|
||||
end
|
||||
end
|
||||
|
||||
class ActionDispatch::IntegrationTest
|
||||
|
@ -5,7 +5,7 @@ http_interactions:
|
||||
uri: https://api.stripe.com/v1/payment_methods
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2022&card[cvc]=314
|
||||
string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2024&card[cvc]=314
|
||||
headers:
|
||||
User-Agent:
|
||||
- Stripe/v1 RubyBindings/5.29.0
|
||||
@ -13,12 +13,14 @@ http_interactions:
|
||||
- Bearer sk_test_testfaketestfaketestfake
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
X-Stripe-Client-Telemetry:
|
||||
- '{"last_request_metrics":{"request_id":"req_MQd4Z7i8cW9FYF","request_duration_ms":535}}'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-Client-User-Agent:
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin
|
||||
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33
|
||||
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}'
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux
|
||||
version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
|
||||
(GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
@ -31,11 +33,11 @@ http_interactions:
|
||||
Server:
|
||||
- nginx
|
||||
Date:
|
||||
- Mon, 13 Sep 2021 11:24:07 GMT
|
||||
- Wed, 15 Mar 2023 11:51:50 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '934'
|
||||
- '930'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Credentials:
|
||||
@ -50,19 +52,23 @@ http_interactions:
|
||||
- '300'
|
||||
Cache-Control:
|
||||
- no-cache, no-store
|
||||
Idempotency-Key:
|
||||
- 8b19d06b-ed60-406d-8490-3d7062d47f67
|
||||
Original-Request:
|
||||
- req_jrCbj5YQwrn3m7
|
||||
Request-Id:
|
||||
- req_tlTlxEJC4LyAQv
|
||||
- req_jrCbj5YQwrn3m7
|
||||
Stripe-Should-Retry:
|
||||
- 'false'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-C-Cost:
|
||||
- '6'
|
||||
Strict-Transport-Security:
|
||||
- max-age=31556926; includeSubDomains; preload
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
string: |-
|
||||
{
|
||||
"id": "pm_1JZDGd2sOmf47Nz9LCckU76B",
|
||||
"id": "pm_1Mlsry2sOmf47Nz9g8twwVyn",
|
||||
"object": "payment_method",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
@ -86,7 +92,7 @@ http_interactions:
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 4,
|
||||
"exp_year": 2022,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "o52jybR7bnmNn6AT",
|
||||
"funding": "credit",
|
||||
"generated_from": null,
|
||||
@ -102,20 +108,19 @@ http_interactions:
|
||||
},
|
||||
"wallet": null
|
||||
},
|
||||
"created": 1631532247,
|
||||
"created": 1678881110,
|
||||
"customer": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"metadata": {},
|
||||
"type": "card"
|
||||
}
|
||||
recorded_at: Mon, 13 Sep 2021 11:24:07 GMT
|
||||
recorded_at: Wed, 15 Mar 2023 11:51:50 GMT
|
||||
- request:
|
||||
method: post
|
||||
uri: https://api.stripe.com/v1/payment_intents
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: payment_method=pm_1JZDGd2sOmf47Nz9LCckU76B&amount=11500¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8Di1wjdVktv5kt
|
||||
string: payment_method=pm_1Mlsry2sOmf47Nz9g8twwVyn&amount=11500¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8Di1wjdVktv5kt
|
||||
headers:
|
||||
User-Agent:
|
||||
- Stripe/v1 RubyBindings/5.29.0
|
||||
@ -124,13 +129,13 @@ http_interactions:
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
X-Stripe-Client-Telemetry:
|
||||
- '{"last_request_metrics":{"request_id":"req_tlTlxEJC4LyAQv","request_duration_ms":663}}'
|
||||
- '{"last_request_metrics":{"request_id":"req_jrCbj5YQwrn3m7","request_duration_ms":586}}'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-Client-User-Agent:
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin
|
||||
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33
|
||||
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}'
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux
|
||||
version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
|
||||
(GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
@ -143,11 +148,11 @@ http_interactions:
|
||||
Server:
|
||||
- nginx
|
||||
Date:
|
||||
- Mon, 13 Sep 2021 11:24:09 GMT
|
||||
- Wed, 15 Mar 2023 11:51:52 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '4263'
|
||||
- '4522'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Credentials:
|
||||
@ -162,25 +167,33 @@ http_interactions:
|
||||
- '300'
|
||||
Cache-Control:
|
||||
- no-cache, no-store
|
||||
Idempotency-Key:
|
||||
- bceb9ff3-545c-48c9-9f9b-4654dd50641b
|
||||
Original-Request:
|
||||
- req_2QEtRciNfNblB1
|
||||
Request-Id:
|
||||
- req_4hAutJ5WkAA9ps
|
||||
- req_2QEtRciNfNblB1
|
||||
Stripe-Should-Retry:
|
||||
- 'false'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-C-Cost:
|
||||
- '10'
|
||||
Strict-Transport-Security:
|
||||
- max-age=31556926; includeSubDomains; preload
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
string: |-
|
||||
{
|
||||
"id": "pi_3JZDGd2sOmf47Nz91tgWkK3L",
|
||||
"id": "pi_3Mlsrz2sOmf47Nz90UCTYKFx",
|
||||
"object": "payment_intent",
|
||||
"amount": 11500,
|
||||
"amount_capturable": 0,
|
||||
"amount_details": {
|
||||
"tip": {}
|
||||
},
|
||||
"amount_received": 11500,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"automatic_payment_methods": null,
|
||||
"canceled_at": null,
|
||||
"cancellation_reason": null,
|
||||
"capture_method": "automatic",
|
||||
@ -188,7 +201,7 @@ http_interactions:
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "ch_3JZDGd2sOmf47Nz91HsbEa5U",
|
||||
"id": "ch_3Mlsrz2sOmf47Nz9004R5HME",
|
||||
"object": "charge",
|
||||
"amount": 11500,
|
||||
"amount_captured": 11500,
|
||||
@ -196,7 +209,7 @@ http_interactions:
|
||||
"application": null,
|
||||
"application_fee": null,
|
||||
"application_fee_amount": null,
|
||||
"balance_transaction": "txn_3JZDGd2sOmf47Nz91MEnox3F",
|
||||
"balance_transaction": "txn_3Mlsrz2sOmf47Nz90SLqRLy1",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
@ -212,34 +225,33 @@ http_interactions:
|
||||
},
|
||||
"calculated_statement_descriptor": "Stripe",
|
||||
"captured": true,
|
||||
"created": 1631532248,
|
||||
"created": 1678881111,
|
||||
"currency": "usd",
|
||||
"customer": "cus_8Di1wjdVktv5kt",
|
||||
"description": null,
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {
|
||||
},
|
||||
"fraud_details": {},
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"metadata": {},
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
"risk_score": 12,
|
||||
"risk_score": 29,
|
||||
"seller_message": "Payment complete.",
|
||||
"type": "authorized"
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_3JZDGd2sOmf47Nz91tgWkK3L",
|
||||
"payment_method": "pm_1JZDGd2sOmf47Nz9LCckU76B",
|
||||
"payment_intent": "pi_3Mlsrz2sOmf47Nz90UCTYKFx",
|
||||
"payment_method": "pm_1Mlsry2sOmf47Nz9g8twwVyn",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@ -250,11 +262,12 @@ http_interactions:
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 4,
|
||||
"exp_year": 2022,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "o52jybR7bnmNn6AT",
|
||||
"funding": "credit",
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"network": "visa",
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
@ -263,16 +276,14 @@ http_interactions:
|
||||
},
|
||||
"receipt_email": null,
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDGd2sOmf47Nz91HsbEa5U/rcpt_KDeZXFrj8mqhXX4v6MKFwawzylc2kPA",
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xMDNyRTYyc09tZjQ3Tno5KNjixqAGMgZncKXPXn06LBY8nUVkDGBhMzC5bIzAPJWsR_S75nAIhMIDBqALhrU35BgWxXMZpcMss_ty",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
|
||||
],
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/ch_3JZDGd2sOmf47Nz91HsbEa5U/refunds"
|
||||
"url": "/v1/charges/ch_3Mlsrz2sOmf47Nz9004R5HME/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
@ -287,25 +298,26 @@ http_interactions:
|
||||
],
|
||||
"has_more": false,
|
||||
"total_count": 1,
|
||||
"url": "/v1/charges?payment_intent=pi_3JZDGd2sOmf47Nz91tgWkK3L"
|
||||
"url": "/v1/charges?payment_intent=pi_3Mlsrz2sOmf47Nz90UCTYKFx"
|
||||
},
|
||||
"client_secret": "pi_3JZDGd2sOmf47Nz91tgWkK3L_secret_DooP6j5YiNN0kzaXPdTGEeKeR",
|
||||
"client_secret": "pi_3Mlsrz2sOmf47Nz90UCTYKFx_secret_af9V81CCyVZZ2J2sptymbRLES",
|
||||
"confirmation_method": "manual",
|
||||
"created": 1631532247,
|
||||
"created": 1678881111,
|
||||
"currency": "usd",
|
||||
"customer": "cus_8Di1wjdVktv5kt",
|
||||
"description": null,
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"latest_charge": "ch_3Mlsrz2sOmf47Nz9004R5HME",
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"metadata": {},
|
||||
"next_action": null,
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1JZDGd2sOmf47Nz9LCckU76B",
|
||||
"payment_method": "pm_1Mlsry2sOmf47Nz9g8twwVyn",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"mandate_options": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
@ -313,6 +325,7 @@ http_interactions:
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"processing": null,
|
||||
"receipt_email": null,
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
@ -324,13 +337,13 @@ http_interactions:
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
recorded_at: Mon, 13 Sep 2021 11:24:09 GMT
|
||||
recorded_at: Wed, 15 Mar 2023 11:51:52 GMT
|
||||
- request:
|
||||
method: post
|
||||
uri: https://api.stripe.com/v1/payment_intents/pi_3JZDGd2sOmf47Nz91tgWkK3L
|
||||
uri: https://api.stripe.com/v1/payment_intents/pi_3Mlsrz2sOmf47Nz90UCTYKFx
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: description=Invoice+reference%3A+2109001%2FVL
|
||||
string: description=Invoice+reference%3A+2303007%2FVL
|
||||
headers:
|
||||
User-Agent:
|
||||
- Stripe/v1 RubyBindings/5.29.0
|
||||
@ -339,13 +352,13 @@ http_interactions:
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
X-Stripe-Client-Telemetry:
|
||||
- '{"last_request_metrics":{"request_id":"req_4hAutJ5WkAA9ps","request_duration_ms":1725}}'
|
||||
- '{"last_request_metrics":{"request_id":"req_2QEtRciNfNblB1","request_duration_ms":1482}}'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-Client-User-Agent:
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin
|
||||
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33
|
||||
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}'
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux
|
||||
version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
|
||||
(GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
@ -358,11 +371,11 @@ http_interactions:
|
||||
Server:
|
||||
- nginx
|
||||
Date:
|
||||
- Mon, 13 Sep 2021 11:24:10 GMT
|
||||
- Wed, 15 Mar 2023 11:51:52 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '4290'
|
||||
- '4549'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Credentials:
|
||||
@ -377,25 +390,33 @@ http_interactions:
|
||||
- '300'
|
||||
Cache-Control:
|
||||
- no-cache, no-store
|
||||
Idempotency-Key:
|
||||
- 51790c68-63e7-4b6e-96b1-4b6eef30b42b
|
||||
Original-Request:
|
||||
- req_Ms4raQ174jWmPK
|
||||
Request-Id:
|
||||
- req_ysWG3JfyCp5xVD
|
||||
- req_Ms4raQ174jWmPK
|
||||
Stripe-Should-Retry:
|
||||
- 'false'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-C-Cost:
|
||||
- '0'
|
||||
Strict-Transport-Security:
|
||||
- max-age=31556926; includeSubDomains; preload
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
string: |-
|
||||
{
|
||||
"id": "pi_3JZDGd2sOmf47Nz91tgWkK3L",
|
||||
"id": "pi_3Mlsrz2sOmf47Nz90UCTYKFx",
|
||||
"object": "payment_intent",
|
||||
"amount": 11500,
|
||||
"amount_capturable": 0,
|
||||
"amount_details": {
|
||||
"tip": {}
|
||||
},
|
||||
"amount_received": 11500,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"automatic_payment_methods": null,
|
||||
"canceled_at": null,
|
||||
"cancellation_reason": null,
|
||||
"capture_method": "automatic",
|
||||
@ -403,7 +424,7 @@ http_interactions:
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "ch_3JZDGd2sOmf47Nz91HsbEa5U",
|
||||
"id": "ch_3Mlsrz2sOmf47Nz9004R5HME",
|
||||
"object": "charge",
|
||||
"amount": 11500,
|
||||
"amount_captured": 11500,
|
||||
@ -411,7 +432,7 @@ http_interactions:
|
||||
"application": null,
|
||||
"application_fee": null,
|
||||
"application_fee_amount": null,
|
||||
"balance_transaction": "txn_3JZDGd2sOmf47Nz91MEnox3F",
|
||||
"balance_transaction": "txn_3Mlsrz2sOmf47Nz90SLqRLy1",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
@ -427,34 +448,33 @@ http_interactions:
|
||||
},
|
||||
"calculated_statement_descriptor": "Stripe",
|
||||
"captured": true,
|
||||
"created": 1631532248,
|
||||
"created": 1678881111,
|
||||
"currency": "usd",
|
||||
"customer": "cus_8Di1wjdVktv5kt",
|
||||
"description": null,
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {
|
||||
},
|
||||
"fraud_details": {},
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"metadata": {},
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
"risk_score": 12,
|
||||
"risk_score": 29,
|
||||
"seller_message": "Payment complete.",
|
||||
"type": "authorized"
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_3JZDGd2sOmf47Nz91tgWkK3L",
|
||||
"payment_method": "pm_1JZDGd2sOmf47Nz9LCckU76B",
|
||||
"payment_intent": "pi_3Mlsrz2sOmf47Nz90UCTYKFx",
|
||||
"payment_method": "pm_1Mlsry2sOmf47Nz9g8twwVyn",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@ -465,11 +485,12 @@ http_interactions:
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 4,
|
||||
"exp_year": 2022,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "o52jybR7bnmNn6AT",
|
||||
"funding": "credit",
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"network": "visa",
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
@ -478,16 +499,14 @@ http_interactions:
|
||||
},
|
||||
"receipt_email": null,
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDGd2sOmf47Nz91HsbEa5U/rcpt_KDeZXFrj8mqhXX4v6MKFwawzylc2kPA",
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xMDNyRTYyc09tZjQ3Tno5KNjixqAGMgbuQcWgG5c6LBa-ZSU_-3ZZaF7tt6uBsD2Rs604Am5ssLMpXbt0FvRIOVw-pVEwxmP_5ACg",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
|
||||
],
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/ch_3JZDGd2sOmf47Nz91HsbEa5U/refunds"
|
||||
"url": "/v1/charges/ch_3Mlsrz2sOmf47Nz9004R5HME/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
@ -502,25 +521,26 @@ http_interactions:
|
||||
],
|
||||
"has_more": false,
|
||||
"total_count": 1,
|
||||
"url": "/v1/charges?payment_intent=pi_3JZDGd2sOmf47Nz91tgWkK3L"
|
||||
"url": "/v1/charges?payment_intent=pi_3Mlsrz2sOmf47Nz90UCTYKFx"
|
||||
},
|
||||
"client_secret": "pi_3JZDGd2sOmf47Nz91tgWkK3L_secret_DooP6j5YiNN0kzaXPdTGEeKeR",
|
||||
"client_secret": "pi_3Mlsrz2sOmf47Nz90UCTYKFx_secret_af9V81CCyVZZ2J2sptymbRLES",
|
||||
"confirmation_method": "manual",
|
||||
"created": 1631532247,
|
||||
"created": 1678881111,
|
||||
"currency": "usd",
|
||||
"customer": "cus_8Di1wjdVktv5kt",
|
||||
"description": "Invoice reference: 2109001/VL",
|
||||
"description": "Invoice reference: 2303007/VL",
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"latest_charge": "ch_3Mlsrz2sOmf47Nz9004R5HME",
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"metadata": {},
|
||||
"next_action": null,
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1JZDGd2sOmf47Nz9LCckU76B",
|
||||
"payment_method": "pm_1Mlsry2sOmf47Nz9g8twwVyn",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"mandate_options": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
@ -528,6 +548,7 @@ http_interactions:
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"processing": null,
|
||||
"receipt_email": null,
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
@ -539,5 +560,5 @@ http_interactions:
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
recorded_at: Mon, 13 Sep 2021 11:24:10 GMT
|
||||
recorded_at: Wed, 15 Mar 2023 11:51:53 GMT
|
||||
recorded_with: VCR 6.0.0
|
||||
|
@ -5,7 +5,7 @@ http_interactions:
|
||||
uri: https://api.stripe.com/v1/payment_methods
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2022&card[cvc]=314
|
||||
string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2024&card[cvc]=314
|
||||
headers:
|
||||
User-Agent:
|
||||
- Stripe/v1 RubyBindings/5.29.0
|
||||
@ -13,14 +13,12 @@ http_interactions:
|
||||
- Bearer sk_test_testfaketestfaketestfake
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
X-Stripe-Client-Telemetry:
|
||||
- '{"last_request_metrics":{"request_id":"req_ysWG3JfyCp5xVD","request_duration_ms":520}}'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-Client-User-Agent:
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin
|
||||
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33
|
||||
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}'
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux
|
||||
version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
|
||||
(GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
@ -33,11 +31,11 @@ http_interactions:
|
||||
Server:
|
||||
- nginx
|
||||
Date:
|
||||
- Mon, 13 Sep 2021 11:24:10 GMT
|
||||
- Wed, 15 Mar 2023 11:51:20 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '934'
|
||||
- '930'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Credentials:
|
||||
@ -52,19 +50,23 @@ http_interactions:
|
||||
- '300'
|
||||
Cache-Control:
|
||||
- no-cache, no-store
|
||||
Idempotency-Key:
|
||||
- 6a183d91-3220-4ad4-913c-f169a75aa488
|
||||
Original-Request:
|
||||
- req_LQKka6p7rniNKT
|
||||
Request-Id:
|
||||
- req_7RNGSU2vySHdHz
|
||||
- req_LQKka6p7rniNKT
|
||||
Stripe-Should-Retry:
|
||||
- 'false'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-C-Cost:
|
||||
- '6'
|
||||
Strict-Transport-Security:
|
||||
- max-age=31556926; includeSubDomains; preload
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
string: |-
|
||||
{
|
||||
"id": "pm_1JZDGg2sOmf47Nz9pfmMaPtb",
|
||||
"id": "pm_1MlsrU2sOmf47Nz9voyfBlTb",
|
||||
"object": "payment_method",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
@ -88,7 +90,7 @@ http_interactions:
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 4,
|
||||
"exp_year": 2022,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "o52jybR7bnmNn6AT",
|
||||
"funding": "credit",
|
||||
"generated_from": null,
|
||||
@ -104,227 +106,11 @@ http_interactions:
|
||||
},
|
||||
"wallet": null
|
||||
},
|
||||
"created": 1631532250,
|
||||
"created": 1678881080,
|
||||
"customer": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"metadata": {},
|
||||
"type": "card"
|
||||
}
|
||||
recorded_at: Mon, 13 Sep 2021 11:24:10 GMT
|
||||
- request:
|
||||
method: post
|
||||
uri: https://api.stripe.com/v1/payment_intents
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: payment_method=pm_1JZDGg2sOmf47Nz9pfmMaPtb&amount=4200¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8Di1wjdVktv5kt
|
||||
headers:
|
||||
User-Agent:
|
||||
- Stripe/v1 RubyBindings/5.29.0
|
||||
Authorization:
|
||||
- Bearer sk_test_testfaketestfaketestfake
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
X-Stripe-Client-Telemetry:
|
||||
- '{"last_request_metrics":{"request_id":"req_7RNGSU2vySHdHz","request_duration_ms":628}}'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-Client-User-Agent:
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin
|
||||
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33
|
||||
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}'
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx
|
||||
Date:
|
||||
- Mon, 13 Sep 2021 11:24:12 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '4258'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Methods:
|
||||
- GET, POST, HEAD, OPTIONS, DELETE
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Access-Control-Expose-Headers:
|
||||
- Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required
|
||||
Access-Control-Max-Age:
|
||||
- '300'
|
||||
Cache-Control:
|
||||
- no-cache, no-store
|
||||
Request-Id:
|
||||
- req_NgGOJxEFd8THv8
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-C-Cost:
|
||||
- '10'
|
||||
Strict-Transport-Security:
|
||||
- max-age=31556926; includeSubDomains; preload
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
{
|
||||
"id": "pi_3JZDGh2sOmf47Nz91FT4yZ2t",
|
||||
"object": "payment_intent",
|
||||
"amount": 4200,
|
||||
"amount_capturable": 0,
|
||||
"amount_received": 4200,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"canceled_at": null,
|
||||
"cancellation_reason": null,
|
||||
"capture_method": "automatic",
|
||||
"charges": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "ch_3JZDGh2sOmf47Nz91FoAsBFe",
|
||||
"object": "charge",
|
||||
"amount": 4200,
|
||||
"amount_captured": 4200,
|
||||
"amount_refunded": 0,
|
||||
"application": null,
|
||||
"application_fee": null,
|
||||
"application_fee_amount": null,
|
||||
"balance_transaction": "txn_3JZDGh2sOmf47Nz91yuRvukb",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "Stripe",
|
||||
"captured": true,
|
||||
"created": 1631532251,
|
||||
"currency": "usd",
|
||||
"customer": "cus_8Di1wjdVktv5kt",
|
||||
"description": null,
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {
|
||||
},
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
"risk_score": 2,
|
||||
"seller_message": "Payment complete.",
|
||||
"type": "authorized"
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_3JZDGh2sOmf47Nz91FT4yZ2t",
|
||||
"payment_method": "pm_1JZDGg2sOmf47Nz9pfmMaPtb",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 4,
|
||||
"exp_year": 2022,
|
||||
"fingerprint": "o52jybR7bnmNn6AT",
|
||||
"funding": "credit",
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"network": "visa",
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
},
|
||||
"receipt_email": null,
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDGh2sOmf47Nz91FoAsBFe/rcpt_KDeZ4pRoBzCyvhebh2wUzvr5fmdZdtD",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
|
||||
],
|
||||
"has_more": false,
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/ch_3JZDGh2sOmf47Nz91FoAsBFe/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
"source_transfer": null,
|
||||
"statement_descriptor": null,
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "succeeded",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"total_count": 1,
|
||||
"url": "/v1/charges?payment_intent=pi_3JZDGh2sOmf47Nz91FT4yZ2t"
|
||||
},
|
||||
"client_secret": "pi_3JZDGh2sOmf47Nz91FT4yZ2t_secret_F3QBmBEtZjcaKblNLMUnLO6hD",
|
||||
"confirmation_method": "manual",
|
||||
"created": 1631532251,
|
||||
"currency": "usd",
|
||||
"customer": "cus_8Di1wjdVktv5kt",
|
||||
"description": null,
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"next_action": null,
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1JZDGg2sOmf47Nz9pfmMaPtb",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
},
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"receipt_email": null,
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
"statement_descriptor": null,
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "succeeded",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
recorded_at: Mon, 13 Sep 2021 11:24:12 GMT
|
||||
recorded_at: Wed, 15 Mar 2023 11:51:20 GMT
|
||||
recorded_with: VCR 6.0.0
|
||||
|
@ -5,7 +5,7 @@ http_interactions:
|
||||
uri: https://api.stripe.com/v1/payment_methods
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2022&card[cvc]=314
|
||||
string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2024&card[cvc]=314
|
||||
headers:
|
||||
User-Agent:
|
||||
- Stripe/v1 RubyBindings/5.29.0
|
||||
@ -13,14 +13,12 @@ http_interactions:
|
||||
- Bearer sk_test_testfaketestfaketestfake
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
X-Stripe-Client-Telemetry:
|
||||
- '{"last_request_metrics":{"request_id":"req_NgGOJxEFd8THv8","request_duration_ms":1772}}'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-Client-User-Agent:
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin
|
||||
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33
|
||||
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}'
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux
|
||||
version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
|
||||
(GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
@ -33,11 +31,11 @@ http_interactions:
|
||||
Server:
|
||||
- nginx
|
||||
Date:
|
||||
- Mon, 13 Sep 2021 11:24:13 GMT
|
||||
- Wed, 15 Mar 2023 11:51:46 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '934'
|
||||
- '930'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Credentials:
|
||||
@ -52,19 +50,23 @@ http_interactions:
|
||||
- '300'
|
||||
Cache-Control:
|
||||
- no-cache, no-store
|
||||
Idempotency-Key:
|
||||
- 4a6e0351-64bd-4ede-a018-7c03e320c3f5
|
||||
Original-Request:
|
||||
- req_njTGdMZNpa6wpG
|
||||
Request-Id:
|
||||
- req_px5zsAdlzgwwSe
|
||||
- req_njTGdMZNpa6wpG
|
||||
Stripe-Should-Retry:
|
||||
- 'false'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-C-Cost:
|
||||
- '6'
|
||||
Strict-Transport-Security:
|
||||
- max-age=31556926; includeSubDomains; preload
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
string: |-
|
||||
{
|
||||
"id": "pm_1JZDGj2sOmf47Nz9S8jhZkFt",
|
||||
"id": "pm_1Mlsru2sOmf47Nz9s3VNHXYt",
|
||||
"object": "payment_method",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
@ -88,7 +90,7 @@ http_interactions:
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 4,
|
||||
"exp_year": 2022,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "o52jybR7bnmNn6AT",
|
||||
"funding": "credit",
|
||||
"generated_from": null,
|
||||
@ -104,20 +106,19 @@ http_interactions:
|
||||
},
|
||||
"wallet": null
|
||||
},
|
||||
"created": 1631532253,
|
||||
"created": 1678881106,
|
||||
"customer": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"metadata": {},
|
||||
"type": "card"
|
||||
}
|
||||
recorded_at: Mon, 13 Sep 2021 11:24:13 GMT
|
||||
recorded_at: Wed, 15 Mar 2023 11:51:46 GMT
|
||||
- request:
|
||||
method: post
|
||||
uri: https://api.stripe.com/v1/payment_intents
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: payment_method=pm_1JZDGj2sOmf47Nz9S8jhZkFt&amount=1500¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8CzHcwBJtlA3IL
|
||||
string: payment_method=pm_1Mlsru2sOmf47Nz9s3VNHXYt&amount=1500¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8CzHcwBJtlA3IL
|
||||
headers:
|
||||
User-Agent:
|
||||
- Stripe/v1 RubyBindings/5.29.0
|
||||
@ -126,13 +127,13 @@ http_interactions:
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
X-Stripe-Client-Telemetry:
|
||||
- '{"last_request_metrics":{"request_id":"req_px5zsAdlzgwwSe","request_duration_ms":619}}'
|
||||
- '{"last_request_metrics":{"request_id":"req_njTGdMZNpa6wpG","request_duration_ms":581}}'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-Client-User-Agent:
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin
|
||||
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33
|
||||
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}'
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux
|
||||
version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
|
||||
(GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
@ -145,11 +146,11 @@ http_interactions:
|
||||
Server:
|
||||
- nginx
|
||||
Date:
|
||||
- Mon, 13 Sep 2021 11:24:15 GMT
|
||||
- Wed, 15 Mar 2023 11:51:48 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '4259'
|
||||
- '4517'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Credentials:
|
||||
@ -164,25 +165,33 @@ http_interactions:
|
||||
- '300'
|
||||
Cache-Control:
|
||||
- no-cache, no-store
|
||||
Idempotency-Key:
|
||||
- f50a36a7-97b5-4d67-9c77-43b5cbddccbe
|
||||
Original-Request:
|
||||
- req_EocoIWBH6tucS4
|
||||
Request-Id:
|
||||
- req_EMDO1Z1Uux0kJb
|
||||
- req_EocoIWBH6tucS4
|
||||
Stripe-Should-Retry:
|
||||
- 'false'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-C-Cost:
|
||||
- '10'
|
||||
Strict-Transport-Security:
|
||||
- max-age=31556926; includeSubDomains; preload
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
string: |-
|
||||
{
|
||||
"id": "pi_3JZDGj2sOmf47Nz90n2aOsuM",
|
||||
"id": "pi_3Mlsrv2sOmf47Nz91hRe49rq",
|
||||
"object": "payment_intent",
|
||||
"amount": 1500,
|
||||
"amount_capturable": 0,
|
||||
"amount_details": {
|
||||
"tip": {}
|
||||
},
|
||||
"amount_received": 1500,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"automatic_payment_methods": null,
|
||||
"canceled_at": null,
|
||||
"cancellation_reason": null,
|
||||
"capture_method": "automatic",
|
||||
@ -190,7 +199,7 @@ http_interactions:
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "ch_3JZDGj2sOmf47Nz90qQzwGqL",
|
||||
"id": "ch_3Mlsrv2sOmf47Nz91760imzX",
|
||||
"object": "charge",
|
||||
"amount": 1500,
|
||||
"amount_captured": 1500,
|
||||
@ -198,7 +207,7 @@ http_interactions:
|
||||
"application": null,
|
||||
"application_fee": null,
|
||||
"application_fee_amount": null,
|
||||
"balance_transaction": "txn_3JZDGj2sOmf47Nz90fOwfgv5",
|
||||
"balance_transaction": "txn_3Mlsrv2sOmf47Nz915VOM7IM",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
@ -214,34 +223,33 @@ http_interactions:
|
||||
},
|
||||
"calculated_statement_descriptor": "Stripe",
|
||||
"captured": true,
|
||||
"created": 1631532254,
|
||||
"created": 1678881107,
|
||||
"currency": "usd",
|
||||
"customer": "cus_8CzHcwBJtlA3IL",
|
||||
"description": null,
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {
|
||||
},
|
||||
"fraud_details": {},
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"metadata": {},
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
"risk_score": 22,
|
||||
"risk_score": 7,
|
||||
"seller_message": "Payment complete.",
|
||||
"type": "authorized"
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_3JZDGj2sOmf47Nz90n2aOsuM",
|
||||
"payment_method": "pm_1JZDGj2sOmf47Nz9S8jhZkFt",
|
||||
"payment_intent": "pi_3Mlsrv2sOmf47Nz91hRe49rq",
|
||||
"payment_method": "pm_1Mlsru2sOmf47Nz9s3VNHXYt",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@ -252,11 +260,12 @@ http_interactions:
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 4,
|
||||
"exp_year": 2022,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "o52jybR7bnmNn6AT",
|
||||
"funding": "credit",
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"network": "visa",
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
@ -265,16 +274,14 @@ http_interactions:
|
||||
},
|
||||
"receipt_email": null,
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDGj2sOmf47Nz90qQzwGqL/rcpt_KDeZMofbRKdRjLxxUw4LZO1LIDJP1sR",
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xMDNyRTYyc09tZjQ3Tno5KNTixqAGMgYuj2m72sQ6LBbKXya9pk9SHbdsAtPEkvHFS51bMvmjrLtHgTEs1uQ8YSkOpfxQRpKaC0PP",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
|
||||
],
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/ch_3JZDGj2sOmf47Nz90qQzwGqL/refunds"
|
||||
"url": "/v1/charges/ch_3Mlsrv2sOmf47Nz91760imzX/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
@ -289,25 +296,26 @@ http_interactions:
|
||||
],
|
||||
"has_more": false,
|
||||
"total_count": 1,
|
||||
"url": "/v1/charges?payment_intent=pi_3JZDGj2sOmf47Nz90n2aOsuM"
|
||||
"url": "/v1/charges?payment_intent=pi_3Mlsrv2sOmf47Nz91hRe49rq"
|
||||
},
|
||||
"client_secret": "pi_3JZDGj2sOmf47Nz90n2aOsuM_secret_WR5cdTATWgOShT8ZHInabONRL",
|
||||
"client_secret": "pi_3Mlsrv2sOmf47Nz91hRe49rq_secret_gLvEqjCexLXGNWDLb7g2NpnfB",
|
||||
"confirmation_method": "manual",
|
||||
"created": 1631532253,
|
||||
"created": 1678881107,
|
||||
"currency": "usd",
|
||||
"customer": "cus_8CzHcwBJtlA3IL",
|
||||
"description": null,
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"latest_charge": "ch_3Mlsrv2sOmf47Nz91760imzX",
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"metadata": {},
|
||||
"next_action": null,
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1JZDGj2sOmf47Nz9S8jhZkFt",
|
||||
"payment_method": "pm_1Mlsru2sOmf47Nz9s3VNHXYt",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"mandate_options": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
@ -315,6 +323,7 @@ http_interactions:
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"processing": null,
|
||||
"receipt_email": null,
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
@ -326,13 +335,13 @@ http_interactions:
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
recorded_at: Mon, 13 Sep 2021 11:24:15 GMT
|
||||
recorded_at: Wed, 15 Mar 2023 11:51:48 GMT
|
||||
- request:
|
||||
method: post
|
||||
uri: https://api.stripe.com/v1/payment_intents/pi_3JZDGj2sOmf47Nz90n2aOsuM
|
||||
uri: https://api.stripe.com/v1/payment_intents/pi_3Mlsrv2sOmf47Nz91hRe49rq
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: description=Invoice+reference%3A+2109001%2FVL
|
||||
string: description=Invoice+reference%3A+2303007%2FVL
|
||||
headers:
|
||||
User-Agent:
|
||||
- Stripe/v1 RubyBindings/5.29.0
|
||||
@ -341,13 +350,13 @@ http_interactions:
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
X-Stripe-Client-Telemetry:
|
||||
- '{"last_request_metrics":{"request_id":"req_EMDO1Z1Uux0kJb","request_duration_ms":1528}}'
|
||||
- '{"last_request_metrics":{"request_id":"req_EocoIWBH6tucS4","request_duration_ms":1593}}'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-Client-User-Agent:
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin
|
||||
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33
|
||||
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}'
|
||||
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux
|
||||
version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
|
||||
(GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
@ -360,11 +369,11 @@ http_interactions:
|
||||
Server:
|
||||
- nginx
|
||||
Date:
|
||||
- Mon, 13 Sep 2021 11:24:15 GMT
|
||||
- Wed, 15 Mar 2023 11:51:49 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '4286'
|
||||
- '4544'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Credentials:
|
||||
@ -379,25 +388,33 @@ http_interactions:
|
||||
- '300'
|
||||
Cache-Control:
|
||||
- no-cache, no-store
|
||||
Idempotency-Key:
|
||||
- 75c78d8d-9df1-4bb7-af1b-7de3aeaa7056
|
||||
Original-Request:
|
||||
- req_MQd4Z7i8cW9FYF
|
||||
Request-Id:
|
||||
- req_Xmih0ndHQjzde4
|
||||
- req_MQd4Z7i8cW9FYF
|
||||
Stripe-Should-Retry:
|
||||
- 'false'
|
||||
Stripe-Version:
|
||||
- '2019-08-14'
|
||||
X-Stripe-C-Cost:
|
||||
- '0'
|
||||
Strict-Transport-Security:
|
||||
- max-age=31556926; includeSubDomains; preload
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
string: |-
|
||||
{
|
||||
"id": "pi_3JZDGj2sOmf47Nz90n2aOsuM",
|
||||
"id": "pi_3Mlsrv2sOmf47Nz91hRe49rq",
|
||||
"object": "payment_intent",
|
||||
"amount": 1500,
|
||||
"amount_capturable": 0,
|
||||
"amount_details": {
|
||||
"tip": {}
|
||||
},
|
||||
"amount_received": 1500,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"automatic_payment_methods": null,
|
||||
"canceled_at": null,
|
||||
"cancellation_reason": null,
|
||||
"capture_method": "automatic",
|
||||
@ -405,7 +422,7 @@ http_interactions:
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "ch_3JZDGj2sOmf47Nz90qQzwGqL",
|
||||
"id": "ch_3Mlsrv2sOmf47Nz91760imzX",
|
||||
"object": "charge",
|
||||
"amount": 1500,
|
||||
"amount_captured": 1500,
|
||||
@ -413,7 +430,7 @@ http_interactions:
|
||||
"application": null,
|
||||
"application_fee": null,
|
||||
"application_fee_amount": null,
|
||||
"balance_transaction": "txn_3JZDGj2sOmf47Nz90fOwfgv5",
|
||||
"balance_transaction": "txn_3Mlsrv2sOmf47Nz915VOM7IM",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
@ -429,34 +446,33 @@ http_interactions:
|
||||
},
|
||||
"calculated_statement_descriptor": "Stripe",
|
||||
"captured": true,
|
||||
"created": 1631532254,
|
||||
"created": 1678881107,
|
||||
"currency": "usd",
|
||||
"customer": "cus_8CzHcwBJtlA3IL",
|
||||
"description": null,
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {
|
||||
},
|
||||
"fraud_details": {},
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"metadata": {},
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
"risk_score": 22,
|
||||
"risk_score": 7,
|
||||
"seller_message": "Payment complete.",
|
||||
"type": "authorized"
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_3JZDGj2sOmf47Nz90n2aOsuM",
|
||||
"payment_method": "pm_1JZDGj2sOmf47Nz9S8jhZkFt",
|
||||
"payment_intent": "pi_3Mlsrv2sOmf47Nz91hRe49rq",
|
||||
"payment_method": "pm_1Mlsru2sOmf47Nz9s3VNHXYt",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@ -467,11 +483,12 @@ http_interactions:
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 4,
|
||||
"exp_year": 2022,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "o52jybR7bnmNn6AT",
|
||||
"funding": "credit",
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"network": "visa",
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
@ -480,16 +497,14 @@ http_interactions:
|
||||
},
|
||||
"receipt_email": null,
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDGj2sOmf47Nz90qQzwGqL/rcpt_KDeZMofbRKdRjLxxUw4LZO1LIDJP1sR",
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xMDNyRTYyc09tZjQ3Tno5KNXixqAGMgYPhCqQTfg6LBaYDHIkkSjReWHiH0dGP8uhn2RZUgWlfTBRya6YV380PXzkqIYppHOEGGj9",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
|
||||
],
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/ch_3JZDGj2sOmf47Nz90qQzwGqL/refunds"
|
||||
"url": "/v1/charges/ch_3Mlsrv2sOmf47Nz91760imzX/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
@ -504,25 +519,26 @@ http_interactions:
|
||||
],
|
||||
"has_more": false,
|
||||
"total_count": 1,
|
||||
"url": "/v1/charges?payment_intent=pi_3JZDGj2sOmf47Nz90n2aOsuM"
|
||||
"url": "/v1/charges?payment_intent=pi_3Mlsrv2sOmf47Nz91hRe49rq"
|
||||
},
|
||||
"client_secret": "pi_3JZDGj2sOmf47Nz90n2aOsuM_secret_WR5cdTATWgOShT8ZHInabONRL",
|
||||
"client_secret": "pi_3Mlsrv2sOmf47Nz91hRe49rq_secret_gLvEqjCexLXGNWDLb7g2NpnfB",
|
||||
"confirmation_method": "manual",
|
||||
"created": 1631532253,
|
||||
"created": 1678881107,
|
||||
"currency": "usd",
|
||||
"customer": "cus_8CzHcwBJtlA3IL",
|
||||
"description": "Invoice reference: 2109001/VL",
|
||||
"description": "Invoice reference: 2303007/VL",
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"latest_charge": "ch_3Mlsrv2sOmf47Nz91760imzX",
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"metadata": {},
|
||||
"next_action": null,
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1JZDGj2sOmf47Nz9S8jhZkFt",
|
||||
"payment_method": "pm_1Mlsru2sOmf47Nz9s3VNHXYt",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"mandate_options": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
@ -530,6 +546,7 @@ http_interactions:
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"processing": null,
|
||||
"receipt_email": null,
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
@ -541,5 +558,5 @@ http_interactions:
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
recorded_at: Mon, 13 Sep 2021 11:24:15 GMT
|
||||
recorded_at: Wed, 15 Mar 2023 11:51:49 GMT
|
||||
recorded_with: VCR 6.0.0
|
||||
|
20
yarn.lock
20
yarn.lock
@ -4626,15 +4626,11 @@ caniuse-api@^3.0.0:
|
||||
lodash.memoize "^4.1.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0:
|
||||
version "1.0.30001397"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001397.tgz"
|
||||
integrity sha512-SW9N2TbCdLf0eiNDRrrQXx2sOkaakNZbCjgNpPyMJJbiOrU5QzMIrXOVMRM1myBXTD5iTkdrtU/EguCrBocHlA==
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001313, caniuse-lite@^1.0.30001332, caniuse-lite@^1.0.30001400:
|
||||
version "1.0.30001460"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz"
|
||||
integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ==
|
||||
|
||||
caniuse-lite@^1.0.30001400:
|
||||
version "1.0.30001439"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz#ab7371faeb4adff4b74dad1718a6fd122e45d9cb"
|
||||
integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A==
|
||||
|
||||
chalk@4.1.1:
|
||||
version "4.1.1"
|
||||
@ -8965,10 +8961,10 @@ react-dom@^17.0.2:
|
||||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-hook-form@^7.30.0:
|
||||
version "7.30.0"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.30.0.tgz#c9e2fd54d3627e43bd94bf38ef549df2e80c1371"
|
||||
integrity sha512-DzjiM6o2vtDGNMB9I4yCqW8J21P314SboNG1O0obROkbg7KVS0I7bMtwSdKyapnCPjHgnxc3L7E5PEdISeEUcQ==
|
||||
react-hook-form@~7.31.3:
|
||||
version "7.31.3"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.31.3.tgz#b61bafb9a7435f91695351a7a9f714d8c4df0121"
|
||||
integrity sha512-NVZdCWViIWXXXlQ3jxVQH0NuNfwPf8A/0KvuCxrM9qxtP1qYosfR2ZudarziFrVOC7eTUbWbm1T4OyYCwv9oSQ==
|
||||
|
||||
react-i18next@^11.15.6:
|
||||
version "11.15.6"
|
||||
|
Loading…
x
Reference in New Issue
Block a user