1
0
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:
Sylvain 2023-03-15 14:59:15 +01:00 committed by GitHub
commit b426a3988a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 2481 additions and 998 deletions

View File

@ -1,5 +1,8 @@
# Changelog Fab-manager # Changelog Fab-manager
- [TODO DEPLOY] `rails db:seed`
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`
## v5.8.2 2023 March 13 ## v5.8.2 2023 March 13
- Improved upgrade script - Improved upgrade script

View File

@ -8,6 +8,8 @@ class API::LocalPaymentController < API::PaymentsController
authorize LocalPaymentContext.new(cart, price[:amount]) 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) render on_payment_success(nil, nil, cart)
end end

View File

@ -55,7 +55,7 @@ class API::PayzenController < API::PaymentsController
def check_cart def check_cart
cart = shopping_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 render json: { cart: 'ok' }, status: :ok
end end

View File

@ -80,11 +80,13 @@ class API::PlansController < API::ApiController
end end
@parameters = @parameters.require(:plan) @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, :training_credit_nb, :ui_weight, :disabled, :monthly_payment, :description, :plan_category_id,
:machines_visibility,
plan_file_attributes: %i[id attachment _destroy], plan_file_attributes: %i[id attachment _destroy],
prices_attributes: %i[id amount], 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 end
end end

View File

@ -19,7 +19,7 @@ class API::StripeController < API::PaymentsController
res = nil # json of the API answer res = nil # json of the API answer
cart = shopping_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?
begin begin
amount = debit_amount(cart) # will contains the amount and the details of each invoice lines 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 def setup_subscription
cart = shopping_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?
service = Stripe::Service.new service = Stripe::Service.new
method = service.attach_method_as_default( method = service.attach_method_as_default(

View File

@ -5,24 +5,34 @@ import { useTranslation } from 'react-i18next';
import { FabButton } from './fab-button'; import { FabButton } from './fab-button';
import { FabModal } from './fab-modal'; import { FabModal } from './fab-modal';
interface EditDestroyButtonsProps { type EditDestroyButtonsCommon = {
onDeleteSuccess: (message: string) => void,
onError: (message: string) => void, onError: (message: string) => void,
onEdit: () => void, onEdit: () => void,
itemId: number, itemId: number,
itemType: string, destroy: (itemId: number) => Promise<void>,
apiDestroy: (itemId: number) => Promise<void>,
confirmationMessage?: string|ReactNode,
className?: string, 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. * 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. * Destroy : shows a modal dialog to ask the user for confirmation about the deletion of the provided item.
* Edit : triggers the provided function. * 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 { t } = useTranslation('admin');
const [deletionModal, setDeletionModal] = useState<boolean>(false); const [deletionModal, setDeletionModal] = useState<boolean>(false);
@ -34,30 +44,41 @@ export const EditDestroyButtons: React.FC<EditDestroyButtonsProps> = ({ onDelete
setDeletionModal(!deletionModal); 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. * The deletion has been confirmed by the user.
* Call the API to trigger the deletion of the given item * Call the API to trigger the deletion of the given item
*/ */
const onDeleteConfirmed = (): void => { const onDeleteConfirmed = (): void => {
apiDestroy(itemId).then(() => { destroy(itemId).then(() => {
onDeleteSuccess(t('app.admin.edit_destroy_buttons.deleted', { TYPE: itemType })); typeof onDeleteSuccess === 'function' && onDeleteSuccess(deleteSuccessMessage || t('app.admin.edit_destroy_buttons.deleted'));
}).catch((error) => { }).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 ( return (
<> <>
<div className={`edit-destroy-buttons ${className || ''}`}> <div className={`edit-destroy-buttons ${className || ''}`}>
<FabButton className='edit-btn' onClick={onEdit}> {showEditButton && <FabButton className='edit-btn' onClick={onEdit}>
<PencilSimple size={iconSize} weight="fill" /> <PencilSimple size={iconSize} weight="fill" />
</FabButton> </FabButton>}
<FabButton type='button' className='delete-btn' onClick={toggleDeletionModal}> <FabButton type='button' className='delete-btn' onClick={handleDestroyRequest}>
<Trash size={iconSize} weight="fill" /> <Trash size={iconSize} weight="fill" />
</FabButton> </FabButton>
</div> </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} isOpen={deletionModal}
toggleModal={toggleDeletionModal} toggleModal={toggleDeletionModal}
closeButton={true} closeButton={true}

View File

@ -58,10 +58,10 @@ export const FormSelect = <TFieldValues extends FieldValues, TContext extends ob
<AbstractSelect ref={ref} <AbstractSelect ref={ref}
classNamePrefix="rs" classNamePrefix="rs"
className="rs" className="rs"
value={options.find(c => c.value === value)} value={value === null ? null : options.find(c => c.value === value)}
onChange={val => { onChange={val => {
onChangeCb(val.value); onChangeCb(val?.value);
onChange(val.value); onChange(val?.value);
}} }}
placeholder={placeholder} placeholder={placeholder}
isDisabled={isDisabled} isDisabled={isDisabled}

View File

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

View File

@ -124,7 +124,7 @@ export const MachineCategoriesList: React.FC<MachineCategoriesListProps> = ({ on
onEdit={editMachineCategory(category)} onEdit={editMachineCategory(category)}
itemId={category.id} itemId={category.id}
itemType={t('app.admin.machine_categories_list.machine_category')} itemType={t('app.admin.machine_categories_list.machine_category')}
apiDestroy={MachineCategoryAPI.destroy} /> destroy={MachineCategoryAPI.destroy} />
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -24,6 +24,10 @@ import { UserPlus } from 'phosphor-react';
import { PartnerModal } from './partner-modal'; import { PartnerModal } from './partner-modal';
import { PlanPricingForm } from './plan-pricing-form'; import { PlanPricingForm } from './plan-pricing-form';
import { AdvancedAccountingForm } from '../accounting/advanced-accounting-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; declare const Application: IApplication;
@ -33,13 +37,14 @@ interface PlanFormProps {
onError: (message: string) => void, onError: (message: string) => void,
onSuccess: (message: string) => void, onSuccess: (message: string) => void,
beforeSubmit?: (data: Plan) => void, beforeSubmit?: (data: Plan) => void,
uiRouter: UIRouter
} }
/** /**
* Form to edit or create subscription plans * Form to edit or create subscription plans
*/ */
export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuccess, beforeSubmit }) => { export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuccess, beforeSubmit, uiRouter }) => {
const { handleSubmit, register, control, formState, setValue } = useForm<Plan>({ defaultValues: { ...plan } }); const { handleSubmit, register, control, formState, setValue, getValues, resetField } = useForm<Plan>({ defaultValues: { ...plan } });
const output = useWatch<Plan>({ control }); // eslint-disable-line const output = useWatch<Plan>({ control }); // eslint-disable-line
const { t } = useTranslation('admin'); const { t } = useTranslation('admin');
@ -51,13 +56,19 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
useEffect(() => { useEffect(() => {
GroupAPI.index({ disabled: false }) 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); .catch(onError);
PlanCategoryAPI.index() 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); .catch(onError);
UserAPI.index({ role: 'partner' }) 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); .catch(onError);
}, []); }, []);
@ -101,7 +112,9 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
* Return the available options for the plan period * Return the available options for the plan period
*/ */
const buildPeriodsOptions = (): Array<SelectOption<string>> => { 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); setValue('partner_id', user.id);
}; };
return ( /**
<div className="plan-form"> * Render the content of the 'subscriptions settings' tab
<form onSubmit={handleSubmit(onSubmit)}> */
<h4>{t('app.admin.plan_form.general_information')}</h4> const renderSettingsTab = () => (
<FormInput register={register} <div className="plan-form-content">
id="base_name" <section>
formState={formState} <header>
rules={{ <p className="title">{t('app.admin.plan_form.description')}</p>
required: true, </header>
maxLength: { value: 24, message: t('app.admin.plan_form.name_max_length') } <div className="content">
}}
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">
<FormInput register={register} <FormInput register={register}
rules={{ required: true, min: 1 }} id="base_name"
disabled={action === 'update'}
formState={formState} formState={formState}
label={t('app.admin.plan_form.number_of_periods')} rules={{
type="number" required: true,
id="interval_count" /> maxLength: { value: 24, message: t('app.admin.plan_form.name_max_length') }
<FormSelect options={buildPeriodsOptions()} }}
control={control} label={t('app.admin.plan_form.name')} />
disabled={action === 'update'} <FormRichText control={control}
onChange={handlePeriodUpdate} formState={formState}
id="interval" id="description"
label={t('app.admin.plan_form.period')} label={t('app.admin.plan_form.description')}
formState={formState} limit={200}
rules={{ required: true }} /> 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> </div>
<h4>{t('app.admin.plan_form.partnership')}</h4> </section>
<div className="partnership">
<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} <FormSwitch control={control}
id="partnership" id="partnership"
disabled={action === 'update'} disabled={action === 'update'}
tooltip={t('app.admin.plan_form.partner_plan_help')}
defaultValue={plan?.type === 'PartnerPlan'} defaultValue={plan?.type === 'PartnerPlan'}
onChange={handlePartnershipChange} onChange={handlePartnershipChange}
formState={formState} formState={formState}
label={t('app.admin.plan_form.partner_plan')} /> label={t('app.admin.plan_form.partner_plan')} />
<FormInput register={register} type="hidden" id="type" defaultValue="Plan" /> <FormInput register={register} type="hidden" id="type" defaultValue="Plan" />
{output.type === 'PartnerPlan' && <div className="partner"> {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" {partners && <FormSelect id="partner_id"
options={partners} options={partners}
control={control} control={control}
formState={formState} formState={formState}
rules={{ required: output.type === 'PartnerPlan' }} rules={{ required: output.type === 'PartnerPlan' }}
tooltip={t('app.admin.plan_form.alert_partner_notification')}
label={t('app.admin.plan_form.notified_partner')} />} label={t('app.admin.plan_form.notified_partner')} />}
{output.partner_id && <FabAlert level="info"> <FabButton className="is-secondary" icon={<UserPlus size={20} />} onClick={tooglePartnerModal}>
{t('app.admin.plan_form.alert_partner_notification')} {t('app.admin.plan_form.new_user')}
</FabAlert>} </FabButton>
</div>} </div>}
</div> </div>
<AdvancedAccountingForm register={register} onError={onError} /> </section>
{action === 'update' && <PlanPricingForm formState={formState}
<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} control={control}
onError={onError} id="plan_category_id"
setValue={setValue} tooltip={t('app.admin.plan_form.category_help')}
register={register} />} label={t('app.admin.plan_form.category')} />}
<FabButton type="submit" className="is-info submit-btn"> <FormInput register={register}
{t('app.admin.plan_form.ACTION_plan', { ACTION: action })} formState={formState}
</FabButton> 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> </form>
<PartnerModal isOpen={isOpenPartnerModal} <PartnerModal isOpen={isOpenPartnerModal}
toggleModal={tooglePartnerModal} toggleModal={tooglePartnerModal}
onError={onError} onError={onError}
@ -281,4 +379,4 @@ const PlanFormWrapper: React.FC<PlanFormProps> = (props) => {
</Loader> </Loader>
); );
}; };
Application.Components.component('planForm', react2angular(PlanFormWrapper, ['action', 'plan', 'onError', 'onSuccess'])); Application.Components.component('planForm', react2angular(PlanFormWrapper, ['action', 'plan', 'onError', 'onSuccess', 'uiRouter']));

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

View File

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

View File

@ -92,32 +92,37 @@ export const PlanPricingForm = <TContext extends object>({ register, control, fo
}; };
return ( return (
<div data-testid="plan-pricing-form"> <section className="plan-pricing-form" data-testid="plan-pricing-form">
<h4>{t('app.admin.plan_pricing_form.prices')}</h4> <header>
{plans && <FormSelect options={plans} <p className="title">{t('app.admin.plan_pricing_form.prices')}</p>
label={t('app.admin.plan_pricing_form.copy_prices_from')} <p className="description">{t('app.admin.plan_pricing_form.about_prices')}</p>
tooltip={t('app.admin.plan_pricing_form.copy_prices_from_help')} </header>
control={control} <div className="content">
onChange={handleCopyPrices} {plans && <FormSelect options={plans}
id="parent_plan_id" />} control={control}
{<FabTabs tabs={[ label={t('app.admin.plan_pricing_form.copy_prices_from')}
machines && { tooltip={t('app.admin.plan_pricing_form.copy_prices_from_help')}
id: 'machines', onChange={handleCopyPrices}
title: t('app.admin.plan_pricing_form.machines'), id="parent_plan_id" />}
content: fields.map((price, index) => { {<FabTabs tabs={[
if (price.priceable_type !== 'Machine') return false; machines && {
return renderPriceElement(price, index); id: 'machines',
}).filter(Boolean) title: t('app.admin.plan_pricing_form.machines'),
}, content: fields.map((price, index) => {
spaces && { if (price.priceable_type !== 'Machine') return false;
id: 'spaces', return renderPriceElement(price, index);
title: t('app.admin.plan_pricing_form.spaces'), }).filter(Boolean)
content: fields.map((price, index) => { },
if (price.priceable_type !== 'Space') return false; spaces && {
return renderPriceElement(price, index); id: 'spaces',
}).filter(Boolean) title: t('app.admin.plan_pricing_form.spaces'),
} content: fields.map((price, index) => {
]} />} if (price.priceable_type !== 'Space') return false;
</div> return renderPriceElement(price, index);
}).filter(Boolean)
}
]} />}
</div>
</section>
); );
}; };

View File

@ -114,7 +114,7 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
onEdit={() => handleRequestEdit(p)} onEdit={() => handleRequestEdit(p)}
itemId={p.id} itemId={p.id}
itemType={t('app.admin.configure_packs_button.pack')} itemType={t('app.admin.configure_packs_button.pack')}
apiDestroy={PrepaidPackAPI.destroy}/> destroy={PrepaidPackAPI.destroy}/>
<FabModal isOpen={isOpen} <FabModal isOpen={isOpen}
toggleModal={toggleModal} toggleModal={toggleModal}
title={t('app.admin.configure_packs_button.edit_pack')} title={t('app.admin.configure_packs_button.edit_pack')}

View File

@ -1,22 +1,23 @@
import * as React from 'react'; import * as React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FabButton } from '../base/fab-button';
import { Product } from '../../models/product'; import { Product } from '../../models/product';
import { PencilSimple, Trash } from 'phosphor-react';
import noImage from '../../../../images/no_image.png'; import noImage from '../../../../images/no_image.png';
import { FabStateLabel } from '../base/fab-state-label'; import { FabStateLabel } from '../base/fab-state-label';
import { ProductPrice } from './product-price'; import { ProductPrice } from './product-price';
import { EditDestroyButtons } from '../base/edit-destroy-buttons';
import ProductAPI from '../../api/product';
interface ProductItemProps { interface ProductItemProps {
product: Product, product: Product,
onEdit: (product: Product) => void, 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 * 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'); 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 * Returns CSS class from stock status
*/ */
@ -80,14 +72,13 @@ export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDel
<ProductPrice product={product} className="price" /> <ProductPrice product={product} className="price" />
</div> </div>
<div className='actions'> <div className='actions'>
<div className='manage'> <EditDestroyButtons onDeleteSuccess={onDelete}
<FabButton className='edit-btn' onClick={editProduct(product)}> className="manage"
<PencilSimple size={20} weight="fill" /> onError={onError}
</FabButton> onEdit={editProduct(product)}
<FabButton className='delete-btn' onClick={deleteProduct(product.id)}> itemId={product.id}
<Trash size={20} weight="fill" /> itemType={t('app.admin.store.product_item.product')}
</FabButton> destroy={ProductAPI.destroy} />
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,11 +1,11 @@
import { useEffect, useState } from 'react'; import { ReactNode, useEffect, useState } from 'react';
import Select from 'react-select'; import Select from 'react-select';
import { PencilSimple, X } from 'phosphor-react'; import { PencilSimple } from 'phosphor-react';
import { useFieldArray, UseFormRegister } from 'react-hook-form'; import { useFieldArray, UseFormRegister } from 'react-hook-form';
import { Control, FormState, UseFormSetValue } from 'react-hook-form/dist/types/form'; import { Control, FormState, UseFormSetValue } from 'react-hook-form/dist/types/form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Product, Product, ProductStockMovement,
stockMovementAllReasons, StockMovementIndex, StockMovementIndexFilter, stockMovementAllReasons, StockMovementIndex, StockMovementIndexFilter,
StockMovementReason, StockMovementReason,
StockType StockType
@ -20,6 +20,7 @@ import FormatLib from '../../lib/format';
import ProductLib from '../../lib/product'; import ProductLib from '../../lib/product';
import { useImmer } from 'use-immer'; import { useImmer } from 'use-immer';
import { FabPagination } from '../base/fab-pagination'; import { FabPagination } from '../base/fab-pagination';
import { FormUnsavedList } from '../form/form-unsaved-list';
interface ProductStockFormProps<TContext extends object> { interface ProductStockFormProps<TContext extends object> {
currentFormValues: Product, 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 ( return (
<div className='product-stock-form'> <div className='product-stock-form'>
<h4>{t('app.admin.store.product_stock_form.stock_up_to_date')}&nbsp; <h4>{t('app.admin.store.product_stock_form.stock_up_to_date')}&nbsp;
@ -178,36 +198,19 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
<span>{t('app.admin.store.product_stock_form.external')}</span> <span>{t('app.admin.store.product_stock_form.external')}</span>
<p>{currentFormValues?.stock?.external}</p> <p>{currentFormValues?.stock?.external}</p>
</div> </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> </div>
{fields.length > 0 && <div className="ongoing-stocks"> <FormUnsavedList fields={fields}
<span className="title">{t('app.admin.store.product_stock_form.ongoing_operations')}</span> className="ongoing-stocks"
<span className="save-notice">{t('app.admin.store.product_stock_form.save_reminder')}</span> onRemove={remove}
{fields.map((newMovement, index) => ( register={register}
<div key={index} className="unsaved-stock-movement stock-item"> title={t('app.admin.store.product_stock_form.ongoing_operations')}
<div className="group"> formAttributeName="product_stock_movements_attributes"
<p>{t(`app.admin.store.product_stock_form.type_${ProductLib.stockMovementType(newMovement.reason)}`)}</p> formAttributes={['stock_type', 'quantity', 'reason']}
</div> renderField={renderOngoingStockMovement}
<div className="group"> saveReminderLabel={t('app.admin.store.product_stock_form.save_reminder')}
<span>{t(`app.admin.store.product_stock_form.${newMovement.stock_type}`)}</span> cancelLabel={t('app.admin.store.product_stock_form.cancel')} />
<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>}
<hr /> <hr />

View File

@ -111,14 +111,9 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError, uiRouter }) =>
}; };
/** Delete a product */ /** Delete a product */
const deleteProduct = async (productId: number): Promise<void> => { const deleteProduct = async (message: string): Promise<void> => {
try { await fetchProducts();
await ProductAPI.destroy(productId); onSuccess(message);
await fetchProducts();
onSuccess(t('app.admin.store.products.successfully_deleted'));
} catch (e) {
onError(t('app.admin.store.products.unable_to_delete') + e);
}
}; };
/** Goto new product page */ /** Goto new product page */
@ -244,6 +239,7 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError, uiRouter }) =>
<ProductItem <ProductItem
key={product.id} key={product.id}
product={product} product={product}
onError={onError}
onEdit={editProduct} onEdit={editProduct}
onDelete={deleteProduct} onDelete={deleteProduct}
/> />

View File

@ -199,7 +199,7 @@ export const Trainings: React.FC<TrainingsProps> = ({ onError, onSuccess }) => {
onEdit={() => toTrainingEdit(training)} onEdit={() => toTrainingEdit(training)}
itemId={training.id} itemId={training.id}
itemType={t('app.admin.trainings.training')} itemType={t('app.admin.trainings.training')}
apiDestroy={TrainingAPI.destroy}/> destroy={TrainingAPI.destroy}/>
</div> </div>
</div> </div>
))} ))}

View File

@ -21,11 +21,14 @@
/** /**
* Controller used in the plan creation form * Controller used in the plan creation form
*/ */
Application.Controllers.controller('NewPlanController', ['$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, planCategories) { function ($scope, $uibModal, groups, prices, partners, CSRF, $state, growl, _t, $uiRouter) {
// protection against request forgery // protection against request forgery
CSRF.setMetaTags(); 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 * 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 * 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', 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, Plan, planCategories) { function ($scope, groups, plans, planPromise, machines, spaces, prices, partners, CSRF, $state, $transition$, growl, $filter, _t, $uiRouter) {
// protection against request forgery // protection against request forgery
CSRF.setMetaTags(); CSRF.setMetaTags();
$scope.suscriptionPlan = cleanPlan(planPromise); $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 * Shows an error message forwarded from a child component
*/ */

View File

@ -12,6 +12,16 @@ export interface Partner {
email: string 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 { export interface Plan {
id?: number, id?: number,
base_name: string, base_name: string,
@ -34,8 +44,10 @@ export interface Plan {
plan_file_url?: string, plan_file_url?: string,
partner_id?: number, partner_id?: number,
partnership?: boolean, partnership?: boolean,
limiting?: boolean,
partners?: Array<Partner>, partners?: Array<Partner>,
advanced_accounting_attributes?: AdvancedAccounting advanced_accounting_attributes?: AdvancedAccounting,
plan_limitations_attributes?: Array<PlanLimitation>
} }
export interface PlansDuration { export interface PlansDuration {

View File

@ -62,6 +62,7 @@
@import "modules/form/form-checklist"; @import "modules/form/form-checklist";
@import "modules/form/form-file-upload"; @import "modules/form/form-file-upload";
@import "modules/form/form-image-upload"; @import "modules/form/form-image-upload";
@import "modules/form/form-unsaved-list";
@import "modules/group/change-group"; @import "modules/group/change-group";
@import "modules/invoices/invoices-settings-panel"; @import "modules/invoices/invoices-settings-panel";
@import "modules/invoices/vat-settings-modal"; @import "modules/invoices/vat-settings-modal";
@ -97,6 +98,9 @@
@import "modules/plan-categories/plan-categories-list"; @import "modules/plan-categories/plan-categories-list";
@import "modules/plans/plan-card"; @import "modules/plans/plan-card";
@import "modules/plans/plan-form"; @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-filter";
@import "modules/plans/plans-list"; @import "modules/plans/plans-list";
@import "modules/prepaid-packs/packs-summary"; @import "modules/prepaid-packs/packs-summary";

View File

@ -12,16 +12,21 @@
text-align: center; text-align: center;
color: var(--main); color: var(--main);
border-bottom: 1px solid var(--gray-soft-dark); 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 { &.react-tabs__tab--selected {
color: var(--gray-hard-dark); color: var(--gray-hard-dark);
border: 1px solid var(--gray-soft-dark); border: 1px solid var(--gray-soft-dark);
border-bottom: none; border-bottom: none;
border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0; &:hover {
} background-color: var(--gray-soft-lightest);
cursor: default;
&:hover { }
cursor: pointer; &:focus { outline: none; }
} }
} }
} }

View File

@ -1,6 +1,6 @@
.fab-text-editor { .fab-text-editor {
position: relative; position: relative;
margin-bottom: 1.6rem; //margin-bottom: 1.6rem;
padding-bottom: 1.6rem; padding-bottom: 1.6rem;
background-color: var(--gray-soft-lightest); background-color: var(--gray-soft-lightest);
border: 1px solid var(--gray-soft-dark); border: 1px solid var(--gray-soft-dark);

View File

@ -15,8 +15,6 @@
flex-direction: column; flex-direction: column;
gap: 3.2rem; gap: 3.2rem;
.fab-alert { margin: 0; }
section { @include layout-settings; } section { @include layout-settings; }
.save-btn { align-self: flex-start; } .save-btn { align-self: flex-start; }
} }
@ -25,7 +23,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
gap: 2.4rem; gap: 1.6rem 2.4rem;
.add-price { .add-price {
max-width: fit-content; max-width: fit-content;
@ -37,6 +35,8 @@
align-items: flex-end; align-items: flex-end;
gap: 0 2.4rem; gap: 0 2.4rem;
.form-item { margin: 0; }
.remove-price { .remove-price {
align-items: center; align-items: center;
display: flex; display: flex;
@ -61,8 +61,7 @@
flex-direction: column; flex-direction: column;
@media (min-width: 640px) {flex-direction: row; } @media (min-width: 640px) {flex-direction: row; }
.form-item:first-child { .form-item { margin: 0; }
margin-right: 2.4rem; .form-item:first-child { margin-right: 2.4rem; }
}
} }
} }

View File

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

View File

@ -15,8 +15,6 @@
flex-direction: column; flex-direction: column;
gap: 3.2rem; gap: 3.2rem;
.fab-alert { margin: 0; }
section { @include layout-settings; } section { @include layout-settings; }
.save-btn { align-self: flex-start; } .save-btn { align-self: flex-start; }
} }

View File

@ -1,25 +1,40 @@
.plan-form { .plan-form {
.plan-sheet { max-width: 1260px;
margin-top: 4rem; 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 { &-content {
margin-right: 32px;
}
}
.partner {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; gap: 3.2rem;
.fab-alert { section { @include layout-settings; }
width: 100%; .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;
}
} }

View File

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

View File

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

View File

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

View File

@ -15,8 +15,6 @@
flex-direction: column; flex-direction: column;
gap: 3.2rem; gap: 3.2rem;
.fab-alert { margin: 0; }
section { @include layout-settings; } section { @include layout-settings; }
.save-btn { align-self: flex-start; } .save-btn { align-self: flex-start; }
} }

View File

@ -3,28 +3,6 @@
.ongoing-stocks { .ongoing-stocks {
margin: 2.4rem 0; 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 { .store-list {
margin-top: 2.4rem; margin-top: 2.4rem;

View File

@ -15,8 +15,6 @@
flex-direction: column; flex-direction: column;
gap: 3.2rem; gap: 3.2rem;
.fab-alert { margin: 0; }
section { @include layout-settings; } section { @include layout-settings; }
.save-btn { align-self: flex-start; } .save-btn { align-self: flex-start; }
} }

View File

@ -29,10 +29,15 @@
} }
& > .content { & > .content {
display: flex;
flex-direction: column;
padding: 1.6rem; padding: 1.6rem;
background-color: var(--gray-soft-light); background-color: var(--gray-soft-light);
border: 1px solid var(--gray-soft-dark); border: 1px solid var(--gray-soft-dark);
border-radius: var(--border-radius); 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) { @media (min-width: 1024px) {

View File

@ -10,24 +10,7 @@
<h1>{{ 'app.admin.plans.edit.subscription_plan' | translate }} {{ suscriptionPlan.base_name }}</h1> <h1>{{ 'app.admin.plans.edit.subscription_plan' | translate }} {{ suscriptionPlan.base_name }}</h1>
</section> </section>
</div> </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> </div>
</section> </section>
<div class="row no-gutter"> <plan-form action="'update'" plan="suscriptionPlan" on-error="onError" on-success="onSuccess" ui-router="uiRouter"></plan-form>
<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>

View File

@ -14,14 +14,4 @@
</div> </div>
</section> </section>
<div class="row no-gutter"> <plan-form action="'create'" on-error="onError" on-success="onSuccess" ui-router="uiRouter"></plan-form>
<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>

View File

@ -13,18 +13,22 @@ class CartItem::Reservation < CartItem::BaseItem
nil nil
end end
# @return [Plan,NilClass]
def plan def plan
nil nil
end end
# @return [User]
def operator def operator
operator_profile.user operator_profile.user
end end
# @return [User]
def customer def customer
customer_profile.user customer_profile.user
end end
# @return [Hash{Symbol=>Integer,Hash{Symbol=>Array<Hash{Symbol=>Integer,Float,Boolean,Time}>}}]
def price def price
is_privileged = operator.privileged? && operator.id != customer.id is_privileged = operator.privileged? && operator.id != customer.id
prepaid = { minutes: PrepaidPackService.minutes_available(customer, reservable) } prepaid = { minutes: PrepaidPackService.minutes_available(customer, reservable) }
@ -48,49 +52,35 @@ class CartItem::Reservation < CartItem::BaseItem
{ elements: elements, amount: amount } { elements: elements, amount: amount }
end end
# @return [String,NilClass]
def name def name
reservable&.name reservable&.name
end end
# @param all_items [Array<CartItem::BaseItem>]
# @return [Boolean]
def valid?(all_items = []) def valid?(all_items = [])
pending_subscription = all_items.find { |i| i.is_a?(CartItem::Subscription) } 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_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| cart_item_reservation_slots.each do |sr|
slot = sr.slot return false unless validate_slot_reservation(sr, pending_subscription, reservation_deadline_minutes, errors)
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
end end
true true
end end
# @return [Reservation]
def to_object def to_object
::Reservation.new( ::Reservation.new(
reservable_id: reservable_id, reservable_id: reservable_id,
@ -107,7 +97,7 @@ class CartItem::Reservation < CartItem::BaseItem
end end
# Group the slots by date, if the extended_prices_in_same_day option is set to true # 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 def grouped_slots
return { all: cart_item_reservation_slots } unless Setting.get('extended_prices_in_same_day') 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] :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 [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 # @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 = {}) def get_slot_price_from_prices(prices, slot_reservation, is_privileged, options = {})
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(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] :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 [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 # @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 = {}) def get_slot_price(hourly_rate, slot_reservation, is_privileged, options = {})
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(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 } } cart_item_reservation_slots.map { |sr| { id: sr.slots_reservation_id, slot_id: sr.slot_id, offered: sr.offered } }
end end
##
# Check if the given availability requires a valid subscription. If so, check if the current customer # 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 # 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) def required_subscription?(availability, pending_subscription)
(customer.subscribed_plan && availability.plan_ids.include?(customer.subscribed_plan.id)) || (customer.subscribed_plan && availability.plan_ids.include?(customer.subscribed_plan.id)) ||
(pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) || (pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) ||
(operator.manager? && customer.id != operator.id) || (operator.manager? && customer.id != operator.id) ||
operator.admin? operator.admin?
end 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 end

View File

@ -42,6 +42,8 @@ class Machine < ApplicationRecord
belongs_to :machine_category 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_statistic_subtype
after_create :create_machine_prices after_create :create_machine_prices
after_create :update_gateway_product after_create :update_gateway_product

View File

@ -4,4 +4,5 @@
class MachineCategory < ApplicationRecord class MachineCategory < ApplicationRecord
has_many :machines, dependent: :nullify has_many :machines, dependent: :nullify
accepts_nested_attributes_for :machines, allow_destroy: true 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 end

View File

@ -21,6 +21,9 @@ class Plan < ApplicationRecord
has_many :cart_item_subscriptions, class_name: 'CartItem::Subscription', dependent: :destroy 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 :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 extend FriendlyId
friendly_id :base_name, use: :slugged friendly_id :base_name, use: :slugged

View 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

View File

@ -32,6 +32,7 @@ class Reservation < ApplicationRecord
after_commit :notify_member_create_reservation, on: :create after_commit :notify_member_create_reservation, on: :create
after_commit :notify_admin_member_create_reservation, on: :create after_commit :notify_admin_member_create_reservation, on: :create
after_commit :extend_subscription, on: :create after_commit :extend_subscription, on: :create
after_commit :notify_member_limitation_reached, on: :create
delegate :user, to: :statistic_profile delegate :user, to: :statistic_profile
@ -137,4 +138,14 @@ class Reservation < ApplicationRecord
receiver: User.admins_and_managers, receiver: User.admins_and_managers,
attached_object: self attached_object: self
end 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 end

View File

@ -160,6 +160,7 @@ class ShoppingCart
# Check if the current cart needs the user to have been validated, and if the condition is satisfied. # 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 an array of errors, if any; false otherwise
# @return [Array<String>,FalseClass]
def check_user_validation(items) def check_user_validation(items)
user_validation_required = Setting.get('user_validation_required') user_validation_required = Setting.get('user_validation_required')
user_validation_required_list = Setting.get('user_validation_required_list') user_validation_required_list = Setting.get('user_validation_required_list')

View File

@ -125,27 +125,17 @@ class Availabilities::AvailabilitiesService
# @param range_end [ActiveSupport::TimeWithZone] # @param range_end [ActiveSupport::TimeWithZone]
# @return ActiveRecord::Relation<Availability> # @return ActiveRecord::Relation<Availability>
def availabilities(availabilities, type, user, range_start, range_end) def availabilities(availabilities, type, user, range_start, range_end)
# who made the request? window = Availabilities::VisibilityService.new.visibility(@current_user, type, range_start, range_end)
# 1) an admin (he can see all availabilities from 1 month ago to anytime in the future) qry = availabilities.includes(:tags, :slots)
if @current_user&.admin? || @current_user&.manager? .joins(:slots)
window_start = [range_start, 1.month.ago].max .where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', window[1], window[0], type)
availabilities.includes(:tags, :slots) .where('slots.start_at > ? AND slots.end_at < ?', window[0], window[1])
.joins(:slots) unless @current_user&.privileged?
.where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', range_end, window_start, type) # non priviledged users cannot see availabilities with tags different than their own and locked tags
.where('slots.start_at > ? AND slots.end_at < ?', window_start, range_end) qry = qry.where('availability_tags.tag_id' => user&.tag_ids&.concat([nil]))
# 2) an user (he cannot see past availabilities neither those further than 1 (or 3) months in the future) .where(lock: false)
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)
end end
qry
end end
end end

View 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

View File

@ -6,10 +6,9 @@ class CartService
@operator = operator @operator = operator
end end
##
# For details about the expected hash format # For details about the expected hash format
# @see app/frontend/src/javascript/models/payment.ts > interface ShoppingCart # @see app/frontend/src/javascript/models/payment.ts > interface ShoppingCart
## # @return [ShoppingCart]
def from_hash(cart_items) def from_hash(cart_items)
cart_items.permit! if cart_items.is_a? ActionController::Parameters cart_items.permit! if cart_items.is_a? ActionController::Parameters

View File

@ -73,7 +73,7 @@ class PrepaidPackService
# Total number of prepaid minutes available # Total number of prepaid minutes available
# @param user [User] # @param user [User]
# @param priceable [Machine,Space] # @param priceable [Machine,Space,NilClass]
def minutes_available(user, priceable) def minutes_available(user, priceable)
return 0 if Setting.get('pack_only_for_subscription') && !user.subscribed_plan return 0 if Setting.get('pack_only_for_subscription') && !user.subscribed_plan

View 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

View File

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

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
json.extract! plan, :id, :base_name, :name, :interval, :interval_count, :group_id, :training_credit_nb, :is_rolling, :description, :type, 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.amount plan.amount / 100.00
json.prices_attributes plan.prices, partial: 'api/prices/price', as: :price json.prices_attributes plan.prices, partial: 'api/prices/price', as: :price
if plan.plan_file if plan.plan_file
@ -27,3 +27,7 @@ if plan.advanced_accounting
end end
end end
json.plan_limitations_attributes plan.plan_limitations do |limitation|
json.extract! limitation, :id, :limitable_id, :limitable_type, :limit
end

View File

@ -1 +1,3 @@
# frozen_string_literal: true
json.partial! 'api/plans/plan', plan: @plan json.partial! 'api/plans/plan', plan: @plan

View File

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

View File

@ -2,8 +2,8 @@ de:
app: app:
admin: admin:
edit_destroy_buttons: edit_destroy_buttons:
deleted: "The {TYPE} was successfully deleted." deleted: "Successfully deleted."
unable_to_delete: "Unable to delete the {TYPE}: " unable_to_delete: "Unable to delete: "
delete_item: "Delete the {TYPE}" delete_item: "Delete the {TYPE}"
confirm_delete: "Delete" confirm_delete: "Delete"
delete_confirmation: "Are you sure you want to delete this {TYPE}?" delete_confirmation: "Are you sure you want to delete this {TYPE}?"
@ -24,7 +24,7 @@ de:
save: "Save" save: "Save"
successfully_saved: "Your banner was successfully saved." successfully_saved: "Your banner was successfully saved."
machine_categories_list: machine_categories_list:
machine_categories: "Machines categories" machine_categories: "Maschinen-Kategorien"
add_a_machine_category: "Add a machine category" add_a_machine_category: "Add a machine category"
name: "Name" name: "Name"
machines_number: "Number of machines" 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." 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: "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." 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" create_success: "The machine was created successfully"
update_success: "The machine was updated successfully" update_success: "The machine was updated successfully"
training_form: 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." 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" default_seats: "Default number of seats"
public_page: "Show in training lists" 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" 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." disabled_help: "When disabled, the training won't be reservable and won't appear by default in the trainings list."
automatic_cancellation: "Automatic cancellation" 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_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_switch: "Activate the validation rule"
validation_rule_period: "Time limit in months" validation_rule_period: "Time limit in months"
save: "Save" save: "Speichern"
create_success: "The training was created successfully" create_success: "Die Schulung wurde erfolgreich erstellt"
update_success: "The training was updated successfully" update_success: "Die Schulung wurde erfolgreich aktualisiert"
space_form: space_form:
ACTION_title: "{ACTION, select, create{New} other{Update the}} space" 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: "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: "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: "Consider changing its prices before creating any reservation slot." consider_changing_its_prices_before_creating_any_reservation_slot: "Ändern Sie ggf. die Preise, bevor Sie Reservierungs-Slots erstellen."
name: "Name" name: "Name"
illustration: "Illustration" illustration: "Abbildung"
description: "Description" description: "Beschreibung"
characteristics: "Characteristics" characteristics: "Eigenschaften"
attachments: "Attachments" attachments: "Dateianhänge"
attached_files_pdf: "Attached files (pdf)" attached_files_pdf: "Angehängte Dateien (pdf)"
add_an_attachment: "Add an attachment" add_an_attachment: "Anhang hinzufügen"
settings: "Settings" settings: "Einstellungen"
default_seats: "Default number of seats" default_seats: "Standardanzahl der Sitze"
disable_space: "Disable the space" disable_space: "Raum deaktivieren"
disabled_help: "When disabled, the space won't be reservable and won't appear by default in the spaces list." disabled_help: "Wenn deaktiviert, ist der Raum nicht reservierbar und erscheint standardmäßig nicht in der Liste der Leerzeichen."
save: "Save" save: "Speichern"
create_success: "The space was created successfully" create_success: "Der Raum wurde erfolgreich erstellt"
update_success: "The space was updated successfully" update_success: "Der Raum wurde erfolgreich aktualisiert"
event_form: event_form:
ACTION_title: "{ACTION, select, create{New} other{Update the}} event" ACTION_title: "{ACTION, select, create{Neue} other{Aktualisiere die}} Veranstaltung"
title: "Title" title: "Titel"
matching_visual: "Matching visual" matching_visual: "Matching visual"
description: "Description" description: "Description"
attachments: "Attachments" attachments: "Attachments"
attached_files_pdf: "Attached files (pdf)" attached_files_pdf: "Attached files (pdf)"
add_a_new_file: "Add a new file" add_a_new_file: "Add a new file"
event_category: "Event category" event_category: "Veranstaltungskategorie"
dates_and_opening_hours: "Dates and opening hours" dates_and_opening_hours: "Dates and opening hours"
all_day: "All day" 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" start_date: "Start date"
end_date: "End date" end_date: "End date"
start_time: "Start time" start_time: "Start time"
@ -135,29 +135,36 @@ de:
fare_class: "Fare class" fare_class: "Fare class"
price: "Price" price: "Price"
seats_available: "Seats available" seats_available: "Seats available"
seats_help: "If you leave this field empty, this event will be available without reservations." seats_help: "Wenn sie dieses Feld leer lassen, ist diese Veranstaltung ohne Reservierung."
event_themes: "Event themes" event_themes: "Veranstaltungsthemen"
age_range: "Age range" age_range: "Age range"
add_price: "Add a price" add_price: "Add a price"
save: "Save" save: "Save"
create_success: "The event was created successfully" create_success: "Die Veranstaltung wurde erfolgreich erstellt"
events_updated: "{COUNT, plural, =1{One event was} other{{COUNT} Events were}} successfully updated" events_updated: "{COUNT, plural, one {}=1{Eine Veranstaltung wurde} other{{COUNT} Veranstaltungen wurden}} erfolgreich aktualisiert"
events_not_updated: "{TOTAL, plural, =1{The event was} other{On {TOTAL} events {COUNT, plural, =1{one was} other{{COUNT} were}}}} not updated." 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: "Unable to remove the requested price because it is associated with some existing reservations" error_deleting_reserved_price: "Der angeforderte Preis konnte nicht gelöscht werden, da er mit einigen Reservierungen verknüpft ist"
other_error: "An unexpected error occurred while updating the event" other_error: "Beim Aktualisieren der Veranstaltung ist ein unerwarteter Fehler aufgetreten"
recurring: recurring:
none: "None" none: "None"
every_days: "Every days" every_days: "Täglich"
every_week: "Every week" every_week: "Wöchentlich"
every_month: "Every month" every_month: "Monatlich"
every_year: "Every year" every_year: "Jährlich"
plan_form: 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: "Name"
name_max_length: "Name length must be less than 24 characters." name_max_length: "Name length must be less than 24 characters."
group: "Group" group: "Gruppe"
transversal: "Transversal plan" transversal: "Transversal plan"
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups." 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: "Category"
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions." category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
number_of_periods: "Number of periods" 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." 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: "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." 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" information_sheet: "Information sheet"
notified_partner: "Notified partner" 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." alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
disabled: "Disable subscription" disabled: "Disable subscription"
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it." 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: "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_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" 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." create_success: "Plan(s) successfully created. Don't forget to redefine prices."
update_success: "The plan was updated successfully" 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: partner_modal:
title: "Create a new partner" title: "Create a new partner"
create_partner: "Create the partner" create_partner: "Create the partner"
@ -196,16 +232,17 @@ de:
email: "Email address" email: "Email address"
plan_pricing_form: plan_pricing_form:
prices: "Prices" 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: "Copy prices from"
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan" copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
machines: "Machines" machines: "Machines"
spaces: "Spaces" spaces: "Spaces"
update_recurrent_modal: update_recurrent_modal:
title: "Periodic event update" title: "Periodic event update"
edit_recurring_event: "You're about to update a periodic event. What do you want to update?" edit_recurring_event: "Sie bearbeiten eine wiederkehrende Veranstaltung. Was möchten Sie ändern?"
edit_this_event: "Only this event" edit_this_event: "Nur diese Veranstaltung"
edit_this_and_next: "This event and the followings" 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." 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}}" confirm: "Update the {MODE, select, single{event} other{events}}"
advanced_accounting_form: advanced_accounting_form:
@ -227,7 +264,7 @@ de:
subscriptions: "Subscriptions" subscriptions: "Subscriptions"
machine: "Machine reservation" machine: "Machine reservation"
training: "Training reservation" training: "Training reservation"
event: "Event reservation" event: "Veranstaltungsreservierung"
space: "Space reservation" space: "Space reservation"
prepaid_pack: "Pack of prepaid-hours" prepaid_pack: "Pack of prepaid-hours"
product: "Product of the store" product: "Product of the store"
@ -286,7 +323,7 @@ de:
manage_trainings: "Klicke hier, um Schulungen hinzuzufügen oder zu entfernen." manage_trainings: "Klicke hier, um Schulungen hinzuzufügen oder zu entfernen."
number_of_tickets: "Anzahl der Tickets: " number_of_tickets: "Anzahl der Tickets: "
adjust_the_opening_hours: "Öffnungszeiten anpassen" 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_options: "Einschränkungsoptionen"
restrict_with_labels: "Diesen Slot mit Labels einschränken" restrict_with_labels: "Diesen Slot mit Labels einschränken"
restrict_for_subscriptions: "Diesen Slot auf Abonnenten einschränken" restrict_for_subscriptions: "Diesen Slot auf Abonnenten einschränken"
@ -508,8 +545,8 @@ de:
on_DATE: "am {DATE}" on_DATE: "am {DATE}"
from_DATE: "von {DATE}" from_DATE: "von {DATE}"
from_TIME: "ab {TIME}" from_TIME: "ab {TIME}"
to_date: "bis" #eg: from 01/01 to 01/05 to_date: "bis" #e.g.: from 01/01 to 01/05
to_time: "bis" #eg. from 18:00 to 21:00 to_time: "bis" #e.g. from 18:00 to 21:00
title: "Titel" title: "Titel"
dates: "Datum" dates: "Datum"
booking: "Buchung" booking: "Buchung"
@ -586,13 +623,13 @@ de:
events_settings: events_settings:
title: "Settings" title: "Settings"
generic_text_block: "Editorial text block" 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" generic_text_block_switch: "Display editorial block"
cta_switch: "Display a button" cta_switch: "Display a button"
cta_label: "Button label" cta_label: "Button label"
cta_url: "url" cta_url: "url"
save: "Save" save: "Save"
update_success: "The events settings were successfully updated" update_success: "Die Einstellungen wurden erfolgreich aktualisiert"
#subscriptions, prices, credits and coupons management #subscriptions, prices, credits and coupons management
pricing: pricing:
pricing_management: "Preisverwaltung" pricing_management: "Preisverwaltung"
@ -1453,8 +1490,8 @@ de:
statistics: "Statistiken" statistics: "Statistiken"
evolution: "Entwicklung" evolution: "Entwicklung"
age_filter: "Altersfilter" age_filter: "Altersfilter"
from_age: "Von" #eg. from 8 to 40 years old from_age: "Von" #e.g. from 8 to 40 years old
to_age: "bis" #eg. from 8 to 40 years old to_age: "bis" #e.g. from 8 to 40 years old
start: "Start:" start: "Start:"
end: "Ende:" end: "Ende:"
custom_filter: "Benutzerderfinierter Filter" custom_filter: "Benutzerderfinierter Filter"
@ -2215,8 +2252,6 @@ de:
unexpected_error_occurred: "An unexpected error occurred. Please try again later." unexpected_error_occurred: "An unexpected error occurred. Please try again later."
all_products: "All products" all_products: "All products"
create_a_product: "Create a product" 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: "Filter"
filter_clear: "Clear all" filter_clear: "Clear all"
filter_apply: "Apply" filter_apply: "Apply"
@ -2238,6 +2273,7 @@ de:
sort: "Sort:" sort: "Sort:"
visible_only: "Visible products only" visible_only: "Visible products only"
product_item: product_item:
product: "product"
visible: "visible" visible: "visible"
hidden: "hidden" hidden: "hidden"
stock: stock:
@ -2288,12 +2324,13 @@ de:
low_stock: "Low stock" low_stock: "Low stock"
threshold_level: "Minimum threshold level" threshold_level: "Minimum threshold level"
threshold_alert: "Notify me when the threshold is reached" threshold_alert: "Notify me when the threshold is reached"
events_history: "Events history" events_history: "Veranstaltungsverlauf"
event_type: "Events:" event_type: "Veranstaltungen:"
reason: "Reason" reason: "Reason"
stocks: "Stock:" stocks: "Stock:"
internal: "Private stock" internal: "Private stock"
external: "Public stock" external: "Public stock"
edit: "Edit"
all: "All types" all: "All types"
remaining_stock: "Remaining stock" remaining_stock: "Remaining stock"
type_in: "Add" type_in: "Add"
@ -2385,7 +2422,7 @@ de:
VAT_rate_machine: "Machine reservation" VAT_rate_machine: "Machine reservation"
VAT_rate_space: "Space reservation" VAT_rate_space: "Space reservation"
VAT_rate_training: "Training reservation" VAT_rate_training: "Training reservation"
VAT_rate_event: "Event reservation" VAT_rate_event: "Veranstaltungsreservierung"
VAT_rate_subscription: "Subscription" VAT_rate_subscription: "Subscription"
VAT_rate_product: "Products (store)" 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." 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."

View File

@ -2,8 +2,8 @@ en:
app: app:
admin: admin:
edit_destroy_buttons: edit_destroy_buttons:
deleted: "The {TYPE} was successfully deleted." deleted: "Successfully deleted."
unable_to_delete: "Unable to delete the {TYPE}: " unable_to_delete: "Unable to delete: "
delete_item: "Delete the {TYPE}" delete_item: "Delete the {TYPE}"
confirm_delete: "Delete" confirm_delete: "Delete"
delete_confirmation: "Are you sure you want to delete this {TYPE}?" delete_confirmation: "Are you sure you want to delete this {TYPE}?"
@ -152,12 +152,19 @@ en:
every_month: "Every month" every_month: "Every month"
every_year: "Every year" every_year: "Every year"
plan_form: 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: "Name"
name_max_length: "Name length must be less than 24 characters." name_max_length: "Name length must be less than 24 characters."
group: "Group" group: "Group"
transversal: "Transversal plan" transversal: "Transversal plan"
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups." 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: "Category"
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions." category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
number_of_periods: "Number of periods" 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." 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: "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." 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" information_sheet: "Information sheet"
notified_partner: "Notified partner" 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." alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
disabled: "Disable subscription" disabled: "Disable subscription"
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it." 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: "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_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" 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." create_success: "Plan(s) successfully created. Don't forget to redefine prices."
update_success: "The plan was updated successfully" 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: partner_modal:
title: "Create a new partner" title: "Create a new partner"
create_partner: "Create the partner" create_partner: "Create the partner"
@ -196,6 +232,7 @@ en:
email: "Email address" email: "Email address"
plan_pricing_form: plan_pricing_form:
prices: "Prices" 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: "Copy prices from"
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan" copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
machines: "Machines" machines: "Machines"
@ -286,7 +323,7 @@ en:
manage_trainings: "Click here to add or remove trainings." manage_trainings: "Click here to add or remove trainings."
number_of_tickets: "Number of tickets: " number_of_tickets: "Number of tickets: "
adjust_the_opening_hours: "Adjust the opening hours" 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_options: "Restriction options"
restrict_with_labels: "Restrict this slot with labels" restrict_with_labels: "Restrict this slot with labels"
restrict_for_subscriptions: "Restrict this slot for subscription users" restrict_for_subscriptions: "Restrict this slot for subscription users"
@ -508,8 +545,8 @@ en:
on_DATE: "on {DATE}" on_DATE: "on {DATE}"
from_DATE: "from {DATE}" from_DATE: "from {DATE}"
from_TIME: "from {TIME}" from_TIME: "from {TIME}"
to_date: "to" #eg: from 01/01 to 01/05 to_date: "to" #e.g.: from 01/01 to 01/05
to_time: "to" #eg. from 18:00 to 21:00 to_time: "to" #e.g. from 18:00 to 21:00
title: "Title" title: "Title"
dates: "Dates" dates: "Dates"
booking: "Booking" booking: "Booking"
@ -1453,8 +1490,8 @@ en:
statistics: "Statistics" statistics: "Statistics"
evolution: "Evolution" evolution: "Evolution"
age_filter: "Age filter" age_filter: "Age filter"
from_age: "From" #eg. from 8 to 40 years old from_age: "From" #e.g. from 8 to 40 years old
to_age: "to" #eg. from 8 to 40 years old to_age: "to" #e.g. from 8 to 40 years old
start: "Start:" start: "Start:"
end: "End:" end: "End:"
custom_filter: "Custom filter" custom_filter: "Custom filter"
@ -2215,8 +2252,6 @@ en:
unexpected_error_occurred: "An unexpected error occurred. Please try again later." unexpected_error_occurred: "An unexpected error occurred. Please try again later."
all_products: "All products" all_products: "All products"
create_a_product: "Create a product" 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: "Filter"
filter_clear: "Clear all" filter_clear: "Clear all"
filter_apply: "Apply" filter_apply: "Apply"
@ -2238,6 +2273,7 @@ en:
sort: "Sort:" sort: "Sort:"
visible_only: "Visible products only" visible_only: "Visible products only"
product_item: product_item:
product: "product"
visible: "visible" visible: "visible"
hidden: "hidden" hidden: "hidden"
stock: stock:
@ -2294,6 +2330,7 @@ en:
stocks: "Stock:" stocks: "Stock:"
internal: "Private stock" internal: "Private stock"
external: "Public stock" external: "Public stock"
edit: "Edit"
all: "All types" all: "All types"
remaining_stock: "Remaining stock" remaining_stock: "Remaining stock"
type_in: "Add" type_in: "Add"

View File

@ -2,8 +2,8 @@ es:
app: app:
admin: admin:
edit_destroy_buttons: edit_destroy_buttons:
deleted: "The {TYPE} was successfully deleted." deleted: "Successfully deleted."
unable_to_delete: "Unable to delete the {TYPE}: " unable_to_delete: "Unable to delete: "
delete_item: "Delete the {TYPE}" delete_item: "Delete the {TYPE}"
confirm_delete: "Delete" confirm_delete: "Delete"
delete_confirmation: "Are you sure you want to delete this {TYPE}?" delete_confirmation: "Are you sure you want to delete this {TYPE}?"
@ -152,12 +152,19 @@ es:
every_month: "Every month" every_month: "Every month"
every_year: "Every year" every_year: "Every year"
plan_form: 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: "Name"
name_max_length: "Name length must be less than 24 characters." name_max_length: "Name length must be less than 24 characters."
group: "Group" group: "Group"
transversal: "Transversal plan" transversal: "Transversal plan"
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups." 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: "Category"
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions." category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
number_of_periods: "Number of periods" 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." 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: "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." 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" information_sheet: "Information sheet"
notified_partner: "Notified partner" 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." alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
disabled: "Disable subscription" disabled: "Disable subscription"
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it." 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: "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_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" 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." create_success: "Plan(s) successfully created. Don't forget to redefine prices."
update_success: "The plan was updated successfully" 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: partner_modal:
title: "Create a new partner" title: "Create a new partner"
create_partner: "Create the partner" create_partner: "Create the partner"
@ -196,6 +232,7 @@ es:
email: "Email address" email: "Email address"
plan_pricing_form: plan_pricing_form:
prices: "Prices" 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: "Copy prices from"
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan" copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
machines: "Machines" machines: "Machines"
@ -286,7 +323,7 @@ es:
manage_trainings: "Click here to add or remove trainings." manage_trainings: "Click here to add or remove trainings."
number_of_tickets: "Número de tickets: " number_of_tickets: "Número de tickets: "
adjust_the_opening_hours: "Ajustar el horario de apertura" 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_options: "Restriction options"
restrict_with_labels: "Restringir este horario con etiquetas" restrict_with_labels: "Restringir este horario con etiquetas"
restrict_for_subscriptions: "Restrict this slot for subscription users" restrict_for_subscriptions: "Restrict this slot for subscription users"
@ -508,8 +545,8 @@ es:
on_DATE: "on {DATE}" on_DATE: "on {DATE}"
from_DATE: "Desde {DATE}" from_DATE: "Desde {DATE}"
from_TIME: "Desde {TIME}" from_TIME: "Desde {TIME}"
to_date: "to" #eg: from 01/01 to 01/05 to_date: "to" #e.g.: from 01/01 to 01/05
to_time: "to" #eg. from 18:00 to 21:00 to_time: "to" #e.g. from 18:00 to 21:00
title: "Title" title: "Title"
dates: "Dates" dates: "Dates"
booking: "Booking" booking: "Booking"
@ -1453,8 +1490,8 @@ es:
statistics: "Statistics" statistics: "Statistics"
evolution: "Evolución" evolution: "Evolución"
age_filter: "Filtro de edad" age_filter: "Filtro de edad"
from_age: "Desde" #eg. from 8 to 40 years old from_age: "Desde" #e.g. from 8 to 40 years old
to_age: "a" #eg. from 8 to 40 years old to_age: "a" #e.g. from 8 to 40 years old
start: "Principio:" start: "Principio:"
end: "Final:" end: "Final:"
custom_filter: "Filtro personalizado" custom_filter: "Filtro personalizado"
@ -2215,8 +2252,6 @@ es:
unexpected_error_occurred: "An unexpected error occurred. Please try again later." unexpected_error_occurred: "An unexpected error occurred. Please try again later."
all_products: "All products" all_products: "All products"
create_a_product: "Create a product" 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: "Filter"
filter_clear: "Clear all" filter_clear: "Clear all"
filter_apply: "Apply" filter_apply: "Apply"
@ -2238,6 +2273,7 @@ es:
sort: "Sort:" sort: "Sort:"
visible_only: "Visible products only" visible_only: "Visible products only"
product_item: product_item:
product: "product"
visible: "visible" visible: "visible"
hidden: "hidden" hidden: "hidden"
stock: stock:
@ -2294,6 +2330,7 @@ es:
stocks: "Stock:" stocks: "Stock:"
internal: "Private stock" internal: "Private stock"
external: "Public stock" external: "Public stock"
edit: "Edit"
all: "All types" all: "All types"
remaining_stock: "Remaining stock" remaining_stock: "Remaining stock"
type_in: "Add" type_in: "Add"

View File

@ -152,12 +152,19 @@ fr:
every_month: "Chaque mois" every_month: "Chaque mois"
every_year: "Chaque année" every_year: "Chaque année"
plan_form: 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: "Nom"
name_max_length: "Le nom doit faire moins de 24 caractères." name_max_length: "Le nom doit faire moins de 24 caractères."
group: "Groupe" group: "Groupe"
transversal: "Abonnement transversal" 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é." 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: "Catégorie"
category_help: "Les catégories vous permettent de regrouper les formules d'abonnement, sur la vue publique des abonnements." 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" 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é." 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: "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." 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" information_sheet: "Fiche descriptive"
notified_partner: "Partenaire notifié" notified_partner: "Partenaire notifié"
new_user: "Nouvel utilisateur ..." new_user: "Nouvel utilisateur ..."
@ -185,9 +191,39 @@ fr:
partner_plan: "Abonnement partenaire" 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_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éé" 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." 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" 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: partner_modal:
title: "Créer un nouveau partenaire" title: "Créer un nouveau partenaire"
create_partner: "Créer le partenaire" create_partner: "Créer le partenaire"
@ -196,6 +232,7 @@ fr:
email: "Adresse électronique" email: "Adresse électronique"
plan_pricing_form: plan_pricing_form:
prices: "Tarifs" 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: "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" 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" machines: "Machines"
@ -286,7 +323,7 @@ fr:
manage_trainings: "Cliquez-ici pour ajouter ou supprimer des formations." manage_trainings: "Cliquez-ici pour ajouter ou supprimer des formations."
number_of_tickets: "Nombre de places : " number_of_tickets: "Nombre de places : "
adjust_the_opening_hours: "Ajuster l'horaire" 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_options: "Options de restriction"
restrict_with_labels: "Restreindre ce créneau avec des étiquettes" restrict_with_labels: "Restreindre ce créneau avec des étiquettes"
restrict_for_subscriptions: "Restreindre ce créneau pour les abonnements" restrict_for_subscriptions: "Restreindre ce créneau pour les abonnements"
@ -508,8 +545,8 @@ fr:
on_DATE: "le {DATE}" on_DATE: "le {DATE}"
from_DATE: "du {DATE}" from_DATE: "du {DATE}"
from_TIME: "de {TIME}" from_TIME: "de {TIME}"
to_date: "au" #eg: from 01/01 to 01/05 to_date: "au" #e.g.: from 01/01 to 01/05
to_time: "à" #eg. from 18:00 to 21:00 to_time: "à" #e.g. from 18:00 to 21:00
title: "Titre" title: "Titre"
dates: "Dates" dates: "Dates"
booking: "Réservations" booking: "Réservations"
@ -1453,8 +1490,8 @@ fr:
statistics: "Statistiques" statistics: "Statistiques"
evolution: "Évolution" evolution: "Évolution"
age_filter: "Filtre d'âge" age_filter: "Filtre d'âge"
from_age: "De" #eg. from 8 to 40 years old from_age: "De" #e.g. from 8 to 40 years old
to_age: "à" #eg. from 8 to 40 years old to_age: "à" #e.g. from 8 to 40 years old
start: "Début :" start: "Début :"
end: "Fin :" end: "Fin :"
custom_filter: "Filtre personnalisé" custom_filter: "Filtre personnalisé"
@ -2215,8 +2252,6 @@ fr:
unexpected_error_occurred: "Une erreur inattendue s'est produite. Veuillez réessayer ultérieurement." unexpected_error_occurred: "Une erreur inattendue s'est produite. Veuillez réessayer ultérieurement."
all_products: "Tous les produits" all_products: "Tous les produits"
create_a_product: "Créer un produit" 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: "Filter"
filter_clear: "Tout effacer" filter_clear: "Tout effacer"
filter_apply: "Appliquer" filter_apply: "Appliquer"
@ -2238,6 +2273,7 @@ fr:
sort: "Trier :" sort: "Trier :"
visible_only: "Produits visibles uniquement" visible_only: "Produits visibles uniquement"
product_item: product_item:
product: "produit"
visible: "visible" visible: "visible"
hidden: "caché" hidden: "caché"
stock: stock:
@ -2294,6 +2330,7 @@ fr:
stocks: "Stock :" stocks: "Stock :"
internal: "Stock interne" internal: "Stock interne"
external: "Stock externe" external: "Stock externe"
edit: "Modifier"
all: "Tous types" all: "Tous types"
remaining_stock: "Stock restant" remaining_stock: "Stock restant"
type_in: "Ajouter" type_in: "Ajouter"

View File

@ -2,8 +2,8 @@
app: app:
admin: admin:
edit_destroy_buttons: edit_destroy_buttons:
deleted: "The {TYPE} was successfully deleted." deleted: "Successfully deleted."
unable_to_delete: "Unable to delete the {TYPE}: " unable_to_delete: "Unable to delete: "
delete_item: "Delete the {TYPE}" delete_item: "Delete the {TYPE}"
confirm_delete: "Delete" confirm_delete: "Delete"
delete_confirmation: "Are you sure you want to delete this {TYPE}?" delete_confirmation: "Are you sure you want to delete this {TYPE}?"
@ -152,12 +152,19 @@
every_month: "Every month" every_month: "Every month"
every_year: "Every year" every_year: "Every year"
plan_form: 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: "Name"
name_max_length: "Name length must be less than 24 characters." name_max_length: "Name length must be less than 24 characters."
group: "Group" group: "Group"
transversal: "Transversal plan" transversal: "Transversal plan"
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups." 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: "Category"
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions." category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
number_of_periods: "Number of periods" 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." 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: "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." 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" information_sheet: "Information sheet"
notified_partner: "Notified partner" 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." alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
disabled: "Disable subscription" disabled: "Disable subscription"
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it." 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: "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_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" 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." create_success: "Plan(s) successfully created. Don't forget to redefine prices."
update_success: "The plan was updated successfully" 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: partner_modal:
title: "Create a new partner" title: "Create a new partner"
create_partner: "Create the partner" create_partner: "Create the partner"
@ -196,6 +232,7 @@
email: "Email address" email: "Email address"
plan_pricing_form: plan_pricing_form:
prices: "Prices" 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: "Copy prices from"
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan" copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
machines: "Machines" machines: "Machines"
@ -286,7 +323,7 @@
manage_trainings: "Klikk her for å legge til eller endre opplæring." manage_trainings: "Klikk her for å legge til eller endre opplæring."
number_of_tickets: "Antall billetter: " number_of_tickets: "Antall billetter: "
adjust_the_opening_hours: "Endre åpningstid" 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_options: "Alternativer for begrensning"
restrict_with_labels: "Begrens denne reservasjonen med etiketter" restrict_with_labels: "Begrens denne reservasjonen med etiketter"
restrict_for_subscriptions: "Begrens denne reservasjoen til medlemmer" restrict_for_subscriptions: "Begrens denne reservasjoen til medlemmer"
@ -508,8 +545,8 @@
on_DATE: "{DATE}" on_DATE: "{DATE}"
from_DATE: "fra {DATE}" from_DATE: "fra {DATE}"
from_TIME: "fra {TIME}" from_TIME: "fra {TIME}"
to_date: "til" #eg: from 01/01 to 01/05 to_date: "til" #e.g.: from 01/01 to 01/05
to_time: "til" #eg. from 18:00 to 21:00 to_time: "til" #e.g. from 18:00 to 21:00
title: "Tittel" title: "Tittel"
dates: "Datoer" dates: "Datoer"
booking: "Reservasjon" booking: "Reservasjon"
@ -1453,8 +1490,8 @@
statistics: "Statistikk" statistics: "Statistikk"
evolution: "Utvikling" evolution: "Utvikling"
age_filter: "Aldersfilter" age_filter: "Aldersfilter"
from_age: "Fra" #eg. from 8 to 40 years old from_age: "Fra" #e.g. from 8 to 40 years old
to_age: "til" #eg. from 8 to 40 years old to_age: "til" #e.g. from 8 to 40 years old
start: "Start:" start: "Start:"
end: "Slutt:" end: "Slutt:"
custom_filter: "Egendefinerte filtre" custom_filter: "Egendefinerte filtre"
@ -2215,8 +2252,6 @@
unexpected_error_occurred: "An unexpected error occurred. Please try again later." unexpected_error_occurred: "An unexpected error occurred. Please try again later."
all_products: "All products" all_products: "All products"
create_a_product: "Create a product" 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: "Filter"
filter_clear: "Clear all" filter_clear: "Clear all"
filter_apply: "Apply" filter_apply: "Apply"
@ -2238,6 +2273,7 @@
sort: "Sort:" sort: "Sort:"
visible_only: "Visible products only" visible_only: "Visible products only"
product_item: product_item:
product: "product"
visible: "visible" visible: "visible"
hidden: "hidden" hidden: "hidden"
stock: stock:
@ -2294,6 +2330,7 @@
stocks: "Stock:" stocks: "Stock:"
internal: "Private stock" internal: "Private stock"
external: "Public stock" external: "Public stock"
edit: "Edit"
all: "All types" all: "All types"
remaining_stock: "Remaining stock" remaining_stock: "Remaining stock"
type_in: "Add" type_in: "Add"

View File

@ -2,8 +2,8 @@ pt:
app: app:
admin: admin:
edit_destroy_buttons: edit_destroy_buttons:
deleted: "The {TYPE} was successfully deleted." deleted: "Successfully deleted."
unable_to_delete: "Unable to delete the {TYPE}: " unable_to_delete: "Unable to delete: "
delete_item: "Delete the {TYPE}" delete_item: "Delete the {TYPE}"
confirm_delete: "Delete" confirm_delete: "Delete"
delete_confirmation: "Are you sure you want to delete this {TYPE}?" delete_confirmation: "Are you sure you want to delete this {TYPE}?"
@ -152,12 +152,19 @@ pt:
every_month: "Every month" every_month: "Every month"
every_year: "Every year" every_year: "Every year"
plan_form: 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: "Name"
name_max_length: "Name length must be less than 24 characters." name_max_length: "Name length must be less than 24 characters."
group: "Group" group: "Group"
transversal: "Transversal plan" transversal: "Transversal plan"
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups." 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: "Category"
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions." category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
number_of_periods: "Number of periods" 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." 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: "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." 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" information_sheet: "Information sheet"
notified_partner: "Notified partner" 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." alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
disabled: "Disable subscription" disabled: "Disable subscription"
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it." 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: "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_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" 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." create_success: "Plan(s) successfully created. Don't forget to redefine prices."
update_success: "The plan was updated successfully" 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: partner_modal:
title: "Create a new partner" title: "Create a new partner"
create_partner: "Create the partner" create_partner: "Create the partner"
@ -196,6 +232,7 @@ pt:
email: "Email address" email: "Email address"
plan_pricing_form: plan_pricing_form:
prices: "Prices" 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: "Copy prices from"
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan" copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
machines: "Machines" machines: "Machines"
@ -286,7 +323,7 @@ pt:
manage_trainings: "Clique aqui para adicionar ou remover treinamentos." manage_trainings: "Clique aqui para adicionar ou remover treinamentos."
number_of_tickets: "Número de vagas: " number_of_tickets: "Número de vagas: "
adjust_the_opening_hours: "Ajustar o horário de funcionamento" 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_options: "Opções de restrição"
restrict_with_labels: "Restrinja este slot com etiquetas" restrict_with_labels: "Restrinja este slot com etiquetas"
restrict_for_subscriptions: "Restringir este slot para os usuários da assinatura" restrict_for_subscriptions: "Restringir este slot para os usuários da assinatura"
@ -508,8 +545,8 @@ pt:
on_DATE: "No {DATE}" on_DATE: "No {DATE}"
from_DATE: "Em {DATE}" from_DATE: "Em {DATE}"
from_TIME: "Ás {TIME}" from_TIME: "Ás {TIME}"
to_date: "ás" #eg: from 01/01 to 01/05 to_date: "ás" #e.g.: from 01/01 to 01/05
to_time: "ás" #eg. from 18:00 to 21:00 to_time: "ás" #e.g. from 18:00 to 21:00
title: "Título" title: "Título"
dates: "Datas" dates: "Datas"
booking: "Reserva" booking: "Reserva"
@ -1453,8 +1490,8 @@ pt:
statistics: "Estatísticas" statistics: "Estatísticas"
evolution: "Evolução" evolution: "Evolução"
age_filter: "Filtro de idade" age_filter: "Filtro de idade"
from_age: "Dos" #eg. from 8 to 40 years old from_age: "Dos" #e.g. from 8 to 40 years old
to_age: "aos" #eg. from 8 to 40 years old to_age: "aos" #e.g. from 8 to 40 years old
start: "Início:" start: "Início:"
end: "Fim:" end: "Fim:"
custom_filter: "Filtro customizado" custom_filter: "Filtro customizado"
@ -2215,8 +2252,6 @@ pt:
unexpected_error_occurred: "An unexpected error occurred. Please try again later." unexpected_error_occurred: "An unexpected error occurred. Please try again later."
all_products: "All products" all_products: "All products"
create_a_product: "Create a product" 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: "Filter"
filter_clear: "Clear all" filter_clear: "Clear all"
filter_apply: "Apply" filter_apply: "Apply"
@ -2238,6 +2273,7 @@ pt:
sort: "Sort:" sort: "Sort:"
visible_only: "Visible products only" visible_only: "Visible products only"
product_item: product_item:
product: "product"
visible: "visible" visible: "visible"
hidden: "hidden" hidden: "hidden"
stock: stock:
@ -2294,6 +2330,7 @@ pt:
stocks: "Stock:" stocks: "Stock:"
internal: "Private stock" internal: "Private stock"
external: "Public stock" external: "Public stock"
edit: "Edit"
all: "All types" all: "All types"
remaining_stock: "Remaining stock" remaining_stock: "Remaining stock"
type_in: "Add" type_in: "Add"

View File

@ -2,8 +2,8 @@ zu:
app: app:
admin: admin:
edit_destroy_buttons: edit_destroy_buttons:
deleted: "crwdns36793:0{TYPE}crwdne36793:0" deleted: "crwdns36793:0crwdne36793:0"
unable_to_delete: "crwdns36795:0{TYPE}crwdne36795:0" unable_to_delete: "crwdns36795:0crwdne36795:0"
delete_item: "crwdns36797:0{TYPE}crwdne36797:0" delete_item: "crwdns36797:0{TYPE}crwdne36797:0"
confirm_delete: "crwdns36799:0crwdne36799:0" confirm_delete: "crwdns36799:0crwdne36799:0"
delete_confirmation: "crwdns36801:0{TYPE}crwdne36801:0" delete_confirmation: "crwdns36801:0{TYPE}crwdne36801:0"
@ -152,12 +152,19 @@ zu:
every_month: "crwdns31885:0crwdne31885:0" every_month: "crwdns31885:0crwdne31885:0"
every_year: "crwdns31887:0crwdne31887:0" every_year: "crwdns31887:0crwdne31887:0"
plan_form: 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: "crwdns31891:0crwdne31891:0"
name_max_length: "crwdns31893:0crwdne31893:0" name_max_length: "crwdns31893:0crwdne31893:0"
group: "crwdns31895:0crwdne31895:0" group: "crwdns31895:0crwdne31895:0"
transversal: "crwdns31897:0crwdne31897:0" transversal: "crwdns31897:0crwdne31897:0"
transversal_help: "crwdns31899:0crwdne31899:0" transversal_help: "crwdns31899:0crwdne31899:0"
display: "crwdns37409:0crwdne37409:0"
category: "crwdns31901:0crwdne31901:0" category: "crwdns31901:0crwdne31901:0"
category_help: "crwdns31903:0crwdne31903:0" category_help: "crwdns31903:0crwdne31903:0"
number_of_periods: "crwdns31905:0crwdne31905:0" number_of_periods: "crwdns31905:0crwdne31905:0"
@ -173,7 +180,6 @@ zu:
rolling_subscription_help: "crwdns31925:0crwdne31925:0" rolling_subscription_help: "crwdns31925:0crwdne31925:0"
monthly_payment: "crwdns31927:0crwdne31927:0" monthly_payment: "crwdns31927:0crwdne31927:0"
monthly_payment_help: "crwdns31929:0crwdne31929:0" monthly_payment_help: "crwdns31929:0crwdne31929:0"
description: "crwdns31931:0crwdne31931:0"
information_sheet: "crwdns31933:0crwdne31933:0" information_sheet: "crwdns31933:0crwdne31933:0"
notified_partner: "crwdns31935:0crwdne31935:0" notified_partner: "crwdns31935:0crwdne31935:0"
new_user: "crwdns31937:0crwdne31937:0" new_user: "crwdns31937:0crwdne31937:0"
@ -185,9 +191,39 @@ zu:
partner_plan: "crwdns31949:0crwdne31949:0" partner_plan: "crwdns31949:0crwdne31949:0"
partner_plan_help: "crwdns31951:0crwdne31951:0" partner_plan_help: "crwdns31951:0crwdne31951:0"
partner_created: "crwdns31953:0crwdne31953: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" create_success: "crwdns31957:0crwdne31957:0"
update_success: "crwdns31959:0crwdne31959: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: partner_modal:
title: "crwdns31961:0crwdne31961:0" title: "crwdns31961:0crwdne31961:0"
create_partner: "crwdns31963:0crwdne31963:0" create_partner: "crwdns31963:0crwdne31963:0"
@ -196,6 +232,7 @@ zu:
email: "crwdns31969:0crwdne31969:0" email: "crwdns31969:0crwdne31969:0"
plan_pricing_form: plan_pricing_form:
prices: "crwdns31971:0crwdne31971:0" prices: "crwdns31971:0crwdne31971:0"
about_prices: "crwdns37465:0crwdne37465:0"
copy_prices_from: "crwdns31973:0crwdne31973:0" copy_prices_from: "crwdns31973:0crwdne31973:0"
copy_prices_from_help: "crwdns31975:0crwdne31975:0" copy_prices_from_help: "crwdns31975:0crwdne31975:0"
machines: "crwdns31977:0crwdne31977:0" machines: "crwdns31977:0crwdne31977:0"
@ -286,7 +323,7 @@ zu:
manage_trainings: "crwdns24132:0crwdne24132:0" manage_trainings: "crwdns24132:0crwdne24132:0"
number_of_tickets: "crwdns24134:0crwdne24134:0" number_of_tickets: "crwdns24134:0crwdne24134:0"
adjust_the_opening_hours: "crwdns24136:0crwdne24136: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_options: "crwdns24140:0crwdne24140:0"
restrict_with_labels: "crwdns24142:0crwdne24142:0" restrict_with_labels: "crwdns24142:0crwdne24142:0"
restrict_for_subscriptions: "crwdns24144:0crwdne24144:0" restrict_for_subscriptions: "crwdns24144:0crwdne24144:0"
@ -508,8 +545,8 @@ zu:
on_DATE: "crwdns24452:0{DATE}crwdne24452:0" on_DATE: "crwdns24452:0{DATE}crwdne24452:0"
from_DATE: "crwdns24454:0{DATE}crwdne24454:0" from_DATE: "crwdns24454:0{DATE}crwdne24454:0"
from_TIME: "crwdns24456:0{TIME}crwdne24456:0" from_TIME: "crwdns24456:0{TIME}crwdne24456:0"
to_date: "crwdns24458:0crwdne24458:0" #eg: from 01/01 to 01/05 to_date: "crwdns24458:0crwdne24458:0" #e.g.: from 01/01 to 01/05
to_time: "crwdns24460:0crwdne24460:0" #eg. from 18:00 to 21:00 to_time: "crwdns24460:0crwdne24460:0" #e.g. from 18:00 to 21:00
title: "crwdns24462:0crwdne24462:0" title: "crwdns24462:0crwdne24462:0"
dates: "crwdns24464:0crwdne24464:0" dates: "crwdns24464:0crwdne24464:0"
booking: "crwdns24466:0crwdne24466:0" booking: "crwdns24466:0crwdne24466:0"
@ -1453,8 +1490,8 @@ zu:
statistics: "crwdns26224:0crwdne26224:0" statistics: "crwdns26224:0crwdne26224:0"
evolution: "crwdns26226:0crwdne26226:0" evolution: "crwdns26226:0crwdne26226:0"
age_filter: "crwdns26228:0crwdne26228:0" age_filter: "crwdns26228:0crwdne26228:0"
from_age: "crwdns26230:0crwdne26230: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" #eg. from 8 to 40 years old to_age: "crwdns26232:0crwdne26232:0" #e.g. from 8 to 40 years old
start: "crwdns26234:0crwdne26234:0" start: "crwdns26234:0crwdne26234:0"
end: "crwdns26236:0crwdne26236:0" end: "crwdns26236:0crwdne26236:0"
custom_filter: "crwdns26238:0crwdne26238:0" custom_filter: "crwdns26238:0crwdne26238:0"
@ -2215,8 +2252,6 @@ zu:
unexpected_error_occurred: "crwdns31338:0crwdne31338:0" unexpected_error_occurred: "crwdns31338:0crwdne31338:0"
all_products: "crwdns31340:0crwdne31340:0" all_products: "crwdns31340:0crwdne31340:0"
create_a_product: "crwdns31342:0crwdne31342:0" create_a_product: "crwdns31342:0crwdne31342:0"
successfully_deleted: "crwdns31344:0crwdne31344:0"
unable_to_delete: "crwdns31346:0crwdne31346:0"
filter: "crwdns31348:0crwdne31348:0" filter: "crwdns31348:0crwdne31348:0"
filter_clear: "crwdns31350:0crwdne31350:0" filter_clear: "crwdns31350:0crwdne31350:0"
filter_apply: "crwdns31352:0crwdne31352:0" filter_apply: "crwdns31352:0crwdne31352:0"
@ -2238,6 +2273,7 @@ zu:
sort: "crwdns31380:0crwdne31380:0" sort: "crwdns31380:0crwdne31380:0"
visible_only: "crwdns31382:0crwdne31382:0" visible_only: "crwdns31382:0crwdne31382:0"
product_item: product_item:
product: "crwdns37467:0crwdne37467:0"
visible: "crwdns31384:0crwdne31384:0" visible: "crwdns31384:0crwdne31384:0"
hidden: "crwdns31386:0crwdne31386:0" hidden: "crwdns31386:0crwdne31386:0"
stock: stock:
@ -2294,6 +2330,7 @@ zu:
stocks: "crwdns31480:0crwdne31480:0" stocks: "crwdns31480:0crwdne31480:0"
internal: "crwdns31482:0crwdne31482:0" internal: "crwdns31482:0crwdne31482:0"
external: "crwdns31484:0crwdne31484:0" external: "crwdns31484:0crwdne31484:0"
edit: "crwdns37469:0crwdne37469:0"
all: "crwdns31486:0crwdne31486:0" all: "crwdns31486:0crwdne31486:0"
remaining_stock: "crwdns31488:0crwdne31488:0" remaining_stock: "crwdns31488:0crwdne31488:0"
type_in: "crwdns31490:0crwdne31490:0" type_in: "crwdns31490:0crwdne31490:0"

View File

@ -540,3 +540,6 @@ de:
show_reserved_uniq: "Show only slots with reservations" show_reserved_uniq: "Show only slots with reservations"
machine: machine:
machine_uncategorized: "Uncategorized machines" machine_uncategorized: "Uncategorized machines"
form_unsaved_list:
save_reminder: "Do not forget to save your changes"
cancel: "Cancel"

View File

@ -540,3 +540,6 @@ en:
show_reserved_uniq: "Show only slots with reservations" show_reserved_uniq: "Show only slots with reservations"
machine: machine:
machine_uncategorized: "Uncategorized machines" machine_uncategorized: "Uncategorized machines"
form_unsaved_list:
save_reminder: "Do not forget to save your changes"
cancel: "Cancel"

View File

@ -540,3 +540,6 @@ es:
show_reserved_uniq: "Show only slots with reservations" show_reserved_uniq: "Show only slots with reservations"
machine: machine:
machine_uncategorized: "Uncategorized machines" machine_uncategorized: "Uncategorized machines"
form_unsaved_list:
save_reminder: "Do not forget to save your changes"
cancel: "Cancel"

View File

@ -540,3 +540,6 @@ fr:
show_reserved_uniq: "Afficher uniquement les créneaux avec réservation" show_reserved_uniq: "Afficher uniquement les créneaux avec réservation"
machine: machine:
machine_uncategorized: "Machines non classés" machine_uncategorized: "Machines non classés"
form_unsaved_list:
save_reminder: "N'oubliez pas d'enregistrer vos modifications"
cancel: "Annuler"

View File

@ -540,3 +540,6 @@
show_reserved_uniq: "Show only slots with reservations" show_reserved_uniq: "Show only slots with reservations"
machine: machine:
machine_uncategorized: "Uncategorized machines" machine_uncategorized: "Uncategorized machines"
form_unsaved_list:
save_reminder: "Do not forget to save your changes"
cancel: "Cancel"

View File

@ -540,3 +540,6 @@ pt:
show_reserved_uniq: "Show only slots with reservations" show_reserved_uniq: "Show only slots with reservations"
machine: machine:
machine_uncategorized: "Uncategorized machines" machine_uncategorized: "Uncategorized machines"
form_unsaved_list:
save_reminder: "Do not forget to save your changes"
cancel: "Cancel"

View File

@ -540,3 +540,6 @@ zu:
show_reserved_uniq: "crwdns36249:0crwdne36249:0" show_reserved_uniq: "crwdns36249:0crwdne36249:0"
machine: machine:
machine_uncategorized: "crwdns36219:0crwdne36219:0" machine_uncategorized: "crwdns36219:0crwdne36219:0"
form_unsaved_list:
save_reminder: "crwdns37471:0crwdne37471:0"
cancel: "crwdns37473:0crwdne37473:0"

View File

@ -433,6 +433,8 @@ de:
schedule_deadline: "Sie müssen den Scheck zur %{DATE} -Frist einlösen, für den Zeitplan %{REFERENCE}" schedule_deadline: "Sie müssen den Scheck zur %{DATE} -Frist einlösen, für den Zeitplan %{REFERENCE}"
notify_admin_payment_schedule_transfer_deadline: notify_admin_payment_schedule_transfer_deadline:
schedule_deadline: "Sie müssen das Lastschriftverfahren für die %{DATE} -Frist bestätigen, für Zeitplan %{REFERENCE}" 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: notify_admin_user_supporting_document_files_created:
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>." supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
notify_admin_user_supporting_document_files_updated: notify_admin_user_supporting_document_files_updated:
@ -519,6 +521,7 @@ de:
availability: "The availaility doesn't exist" availability: "The availaility doesn't exist"
full: "The slot is already fully reserved" full: "The slot is already fully reserved"
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start" 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" restricted: "This availability is restricted for subscribers"
plan: "This subscription plan is disabled" plan: "This subscription plan is disabled"
plan_group: "This subscription plan is reserved for members of group %{GROUP}" plan_group: "This subscription plan is reserved for members of group %{GROUP}"

View File

@ -433,6 +433,8 @@ en:
schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}" schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}"
notify_admin_payment_schedule_transfer_deadline: notify_admin_payment_schedule_transfer_deadline:
schedule_deadline: "You must confirm the bank direct debit for the %{DATE} deadline, for schedule %{REFERENCE}" 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: notify_admin_user_supporting_document_files_created:
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>." supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
notify_admin_user_supporting_document_files_updated: notify_admin_user_supporting_document_files_updated:
@ -519,6 +521,7 @@ en:
availability: "The availaility doesn't exist" availability: "The availaility doesn't exist"
full: "The slot is already fully reserved" full: "The slot is already fully reserved"
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start" 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" restricted: "This availability is restricted for subscribers"
plan: "This subscription plan is disabled" plan: "This subscription plan is disabled"
plan_group: "This subscription plan is reserved for members of group %{GROUP}" plan_group: "This subscription plan is reserved for members of group %{GROUP}"

View File

@ -433,6 +433,8 @@ es:
schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}" schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}"
notify_admin_payment_schedule_transfer_deadline: notify_admin_payment_schedule_transfer_deadline:
schedule_deadline: "You must confirm the bank direct debit for the %{DATE} deadline, for schedule %{REFERENCE}" 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: notify_admin_user_supporting_document_files_created:
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>." supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
notify_admin_user_supporting_document_files_updated: notify_admin_user_supporting_document_files_updated:
@ -519,6 +521,7 @@ es:
availability: "The availaility doesn't exist" availability: "The availaility doesn't exist"
full: "The slot is already fully reserved" full: "The slot is already fully reserved"
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start" 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" restricted: "This availability is restricted for subscribers"
plan: "This subscription plan is disabled" plan: "This subscription plan is disabled"
plan_group: "This subscription plan is reserved for members of group %{GROUP}" plan_group: "This subscription plan is reserved for members of group %{GROUP}"

View File

@ -433,6 +433,8 @@ fr:
schedule_deadline: "Vous devez encaisser le chèque de l'échéance du %{DATE}, pour l'échéancier %{REFERENCE}" 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: 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}" 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: 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." 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: notify_admin_user_supporting_document_files_updated:
@ -519,6 +521,7 @@ fr:
availability: "La disponibilité n'existe pas" availability: "La disponibilité n'existe pas"
full: "Le créneau est déjà entièrement réservé" 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" 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" restricted: "Cette disponibilité n'est disponible que pour les abonnés"
plan: "Cette formule d'abonnement est désactivé" plan: "Cette formule d'abonnement est désactivé"
plan_group: "Cette formule d'abonnement est réservée aux membres du groupe %{GROUP}" plan_group: "Cette formule d'abonnement est réservée aux membres du groupe %{GROUP}"

View File

@ -375,6 +375,10 @@ de:
remember: "Gemäß Ihrem Zahlungsplan von %{REFERENCE} wurde zum %{DATE} eine Belastung der Karte in Höhe von %{AMOUNT} geplant." 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." 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." 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: notify_admin_user_supporting_document_files_created:
subject: "Supporting documents uploaded by a member" subject: "Supporting documents uploaded by a member"
body: body:

View File

@ -375,6 +375,10 @@ en:
remember: "In accordance with your %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}." 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." 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." 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: notify_admin_user_supporting_document_files_created:
subject: "Supporting documents uploaded by a member" subject: "Supporting documents uploaded by a member"
body: body:

View File

@ -375,6 +375,10 @@ es:
remember: "In accordance with your %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}." 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." 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." 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: notify_admin_user_supporting_document_files_created:
subject: "Supporting documents uploaded by a member" subject: "Supporting documents uploaded by a member"
body: body:

View File

@ -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}." 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é." 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." 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: notify_admin_user_supporting_document_files_created:
subject: "Justificatif téléversé par un membre" subject: "Justificatif téléversé par un membre"
body: body:

View File

@ -375,6 +375,10 @@
remember: "In accordance with your %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}." 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." 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." 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: notify_admin_user_supporting_document_files_created:
subject: "Supporting documents uploaded by a member" subject: "Supporting documents uploaded by a member"
body: body:

View File

@ -375,6 +375,10 @@ pt:
remember: "De acordo com a agenda de pagamento %{REFERENCE}, %{AMOUNT} deveria ser debitado em %{DATE}." 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." 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." 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: notify_admin_user_supporting_document_files_created:
subject: "Supporting documents uploaded by a member" subject: "Supporting documents uploaded by a member"
body: body:

View File

@ -375,6 +375,10 @@ zu:
remember: "crwdns29938:0%{REFERENCE}crwdnd29938:0%{AMOUNT}crwdnd29938:0%{DATE}crwdne29938:0" remember: "crwdns29938:0%{REFERENCE}crwdnd29938:0%{AMOUNT}crwdnd29938:0%{DATE}crwdne29938:0"
date: "crwdns29940:0crwdne29940:0" date: "crwdns29940:0crwdne29940:0"
confirm: "crwdns29942:0crwdne29942: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: notify_admin_user_supporting_document_files_created:
subject: "crwdns37349:0crwdne37349:0" subject: "crwdns37349:0crwdne37349:0"
body: body:

View File

@ -433,6 +433,8 @@
schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}" schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}"
notify_admin_payment_schedule_transfer_deadline: notify_admin_payment_schedule_transfer_deadline:
schedule_deadline: "You must confirm the bank direct debit for the %{DATE} deadline, for schedule %{REFERENCE}" 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: notify_admin_user_supporting_document_files_created:
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>." supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
notify_admin_user_supporting_document_files_updated: notify_admin_user_supporting_document_files_updated:
@ -519,6 +521,7 @@
availability: "The availaility doesn't exist" availability: "The availaility doesn't exist"
full: "The slot is already fully reserved" full: "The slot is already fully reserved"
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start" 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" restricted: "This availability is restricted for subscribers"
plan: "This subscription plan is disabled" plan: "This subscription plan is disabled"
plan_group: "This subscription plan is reserved for members of group %{GROUP}" plan_group: "This subscription plan is reserved for members of group %{GROUP}"

View File

@ -433,6 +433,8 @@ pt:
schedule_deadline: "Você deve realizar a verificação para a data limite de %{DATE} para agendar %{REFERENCE}" schedule_deadline: "Você deve realizar a verificação para a data limite de %{DATE} para agendar %{REFERENCE}"
notify_admin_payment_schedule_transfer_deadline: 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}" 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: notify_admin_user_supporting_document_files_created:
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>." supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
notify_admin_user_supporting_document_files_updated: notify_admin_user_supporting_document_files_updated:
@ -519,6 +521,7 @@ pt:
availability: "The availaility doesn't exist" availability: "The availaility doesn't exist"
full: "The slot is already fully reserved" full: "The slot is already fully reserved"
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start" 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" restricted: "This availability is restricted for subscribers"
plan: "This subscription plan is disabled" plan: "This subscription plan is disabled"
plan_group: "This subscription plan is reserved for members of group %{GROUP}" plan_group: "This subscription plan is reserved for members of group %{GROUP}"

View File

@ -433,6 +433,8 @@ zu:
schedule_deadline: "crwdns21120:0%{DATE}crwdnd21120:0%{REFERENCE}crwdne21120:0" schedule_deadline: "crwdns21120:0%{DATE}crwdnd21120:0%{REFERENCE}crwdne21120:0"
notify_admin_payment_schedule_transfer_deadline: notify_admin_payment_schedule_transfer_deadline:
schedule_deadline: "crwdns22305:0%{DATE}crwdnd22305:0%{REFERENCE}crwdne22305:0" 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: notify_admin_user_supporting_document_files_created:
supporting_document_files_uploaded: "crwdns37341:0%{NAME}crwdne37341:0" supporting_document_files_uploaded: "crwdns37341:0%{NAME}crwdne37341:0"
notify_admin_user_supporting_document_files_updated: notify_admin_user_supporting_document_files_updated:
@ -519,6 +521,7 @@ zu:
availability: "crwdns36269:0crwdne36269:0" availability: "crwdns36269:0crwdne36269:0"
full: "crwdns36271:0crwdne36271:0" full: "crwdns36271:0crwdne36271:0"
deadline: "crwdns36273:0%{MINUTES}crwdne36273:0" deadline: "crwdns36273:0%{MINUTES}crwdne36273:0"
limit_reached: "crwdns37475:0%{HOURS}crwdnd37475:0%{RESERVABLE}crwdne37475:0"
restricted: "crwdns36275:0crwdne36275:0" restricted: "crwdns36275:0crwdne36275:0"
plan: "crwdns36277:0crwdne36277:0" plan: "crwdns36277:0crwdne36277:0"
plan_group: "crwdns37207:0%{GROUP}crwdne37207:0" plan_group: "crwdns37207:0%{GROUP}crwdne37207:0"

View 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

View 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

View File

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

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "fuzzystrmatch" enable_extension "fuzzystrmatch"
@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
enable_extension "unaccent" enable_extension "unaccent"
create_table "abuses", id: :serial, force: :cascade do |t| create_table "abuses", id: :serial, force: :cascade do |t|
t.integer "signaled_id"
t.string "signaled_type" t.string "signaled_type"
t.integer "signaled_id"
t.string "first_name" t.string "first_name"
t.string "last_name" t.string "last_name"
t.string "email" t.string "email"
@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
t.string "locality" t.string "locality"
t.string "country" t.string "country"
t.string "postal_code" t.string "postal_code"
t.integer "placeable_id"
t.string "placeable_type" t.string "placeable_type"
t.integer "placeable_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
end end
@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
end end
create_table "assets", id: :serial, force: :cascade do |t| create_table "assets", id: :serial, force: :cascade do |t|
t.integer "viewable_id"
t.string "viewable_type" t.string "viewable_type"
t.integer "viewable_id"
t.string "attachment" t.string "attachment"
t.string "type" t.string "type"
t.datetime "created_at" 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| create_table "cart_item_event_reservation_tickets", force: :cascade do |t|
t.integer "booked" t.integer "booked"
t.bigint "event_price_category_id"
t.bigint "cart_item_event_reservation_id" t.bigint "cart_item_event_reservation_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_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 ["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" t.index ["event_price_category_id"], name: "index_cart_item_tickets_on_event_price_category"
end end
@ -282,8 +282,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
end end
create_table "credits", id: :serial, force: :cascade do |t| create_table "credits", id: :serial, force: :cascade do |t|
t.integer "creditable_id"
t.string "creditable_type" t.string "creditable_type"
t.integer "creditable_id"
t.integer "plan_id" t.integer "plan_id"
t.integer "hours" t.integer "hours"
t.datetime "created_at" 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| create_table "notifications", id: :serial, force: :cascade do |t|
t.integer "receiver_id" t.integer "receiver_id"
t.integer "attached_object_id"
t.string "attached_object_type" t.string "attached_object_type"
t.integer "attached_object_id"
t.integer "notification_type_id" t.integer "notification_type_id"
t.boolean "is_read", default: false t.boolean "is_read", default: false
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "receiver_type" t.string "receiver_type"
t.boolean "is_send", default: false 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 ["notification_type_id"], name: "index_notifications_on_notification_type_id"
t.index ["receiver_id"], name: "index_notifications_on_receiver_id" t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
end end
@ -740,13 +740,14 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
end end
create_table "plan_limitations", force: :cascade do |t| create_table "plan_limitations", force: :cascade do |t|
t.bigint "plan_id" t.bigint "plan_id", null: false
t.string "limitable_type" t.string "limitable_type", null: false
t.bigint "limitable_id" t.bigint "limitable_id", null: false
t.integer "limit", default: 0, null: false t.integer "limit", default: 0, null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_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 ["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" t.index ["plan_id"], name: "index_plan_limitations_on_plan_id"
end end
@ -770,6 +771,7 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
t.boolean "monthly_payment" t.boolean "monthly_payment"
t.bigint "plan_category_id" t.bigint "plan_category_id"
t.boolean "limiting" t.boolean "limiting"
t.integer "machines_visibility"
t.index ["group_id"], name: "index_plans_on_group_id" t.index ["group_id"], name: "index_plans_on_group_id"
t.index ["plan_category_id"], name: "index_plans_on_plan_category_id" t.index ["plan_category_id"], name: "index_plans_on_plan_category_id"
end end
@ -811,15 +813,14 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do
t.text "conditions" t.text "conditions"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_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 t.index ["name"], name: "index_price_categories_on_name", unique: true
end end
create_table "prices", id: :serial, force: :cascade do |t| create_table "prices", id: :serial, force: :cascade do |t|
t.integer "group_id" t.integer "group_id"
t.integer "plan_id" t.integer "plan_id"
t.integer "priceable_id"
t.string "priceable_type" t.string "priceable_type"
t.integer "priceable_id"
t.integer "amount" t.integer "amount"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_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.text "message"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "reservable_id"
t.string "reservable_type" t.string "reservable_type"
t.integer "reservable_id"
t.integer "nb_reserve_places" t.integer "nb_reserve_places"
t.integer "statistic_profile_id" t.integer "statistic_profile_id"
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_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| create_table "roles", id: :serial, force: :cascade do |t|
t.string "name" t.string "name"
t.integer "resource_id"
t.string "resource_type" t.string "resource_type"
t.integer "resource_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"

View File

@ -23,3 +23,11 @@ unless NotificationType.find_by(name: 'notify_admin_order_is_paid')
is_configurable: true is_configurable: true
) )
end 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

View File

@ -161,7 +161,7 @@
"react-cool-onclickoutside": "^1.7.0", "react-cool-onclickoutside": "^1.7.0",
"react-custom-events": "^1.1.1", "react-custom-events": "^1.1.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-hook-form": "^7.30.0", "react-hook-form": "~7.31.3",
"react-i18next": "^11.15.6", "react-i18next": "^11.15.6",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-select": "^5.3.2", "react-select": "^5.3.2",

View File

@ -34,7 +34,7 @@ list_files() {
update_file() { update_file() {
# params: FILE_ID, STORAGE_ID # 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() { find_file_id() {

View File

@ -1,3 +1,4 @@
# admin without subscription
user_1: user_1:
id: 1 id: 1
username: admin username: admin
@ -30,6 +31,7 @@ user_1:
merged_at: merged_at:
is_allow_newsletter: true is_allow_newsletter: true
# member without subscription
user_2: user_2:
id: 2 id: 2
username: jdupond username: jdupond
@ -62,6 +64,7 @@ user_2:
merged_at: merged_at:
is_allow_newsletter: true is_allow_newsletter: true
# member with 1 month subscription (plan 2/standard)
user_3: user_3:
id: 3 id: 3
username: pdurand username: pdurand
@ -94,6 +97,7 @@ user_3:
merged_at: merged_at:
is_allow_newsletter: false is_allow_newsletter: false
# member with 1 month subscription (plan 3/students)
user_4: user_4:
id: 4 id: 4
username: kdumas username: kdumas
@ -126,6 +130,7 @@ user_4:
merged_at: merged_at:
is_allow_newsletter: false is_allow_newsletter: false
# member with 10€ on wallet
user_5: user_5:
id: 5 id: 5
username: vlonchamp username: vlonchamp
@ -158,6 +163,7 @@ user_5:
merged_at: merged_at:
is_allow_newsletter: true is_allow_newsletter: true
# partner of plan 2
user_6: user_6:
id: 6 id: 6
username: GilbertPartenaire username: GilbertPartenaire
@ -190,6 +196,7 @@ user_6:
merged_at: merged_at:
is_allow_newsletter: true is_allow_newsletter: true
# member with 255€ on wallet
user_7: user_7:
id: 7 id: 7
username: lseguin username: lseguin
@ -286,6 +293,7 @@ user_9:
merged_at: merged_at:
is_allow_newsletter: true is_allow_newsletter: true
# member with 1 year subscription
user_10: user_10:
id: 10 id: 10
username: acamus username: acamus

View File

@ -7,6 +7,7 @@ import userEvent from '@testing-library/user-event';
import plans from '../../__fixtures__/plans'; import plans from '../../__fixtures__/plans';
import machines from '../../__fixtures__/machines'; import machines from '../../__fixtures__/machines';
import { tiptapEvent } from '../../__lib__/tiptap'; import { tiptapEvent } from '../../__lib__/tiptap';
import { uiRouter } from '../../__lib__/ui-router';
describe('PlanForm', () => { describe('PlanForm', () => {
const onError = jest.fn(); const onError = jest.fn();
@ -14,7 +15,7 @@ describe('PlanForm', () => {
const beforeSubmit = jest.fn(); const beforeSubmit = jest.fn();
test('render create PlanForm', async () => { 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/ })); 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.name/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.plan_form.transversal/)).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.period/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.plan_form.partner_plan/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.plan_form.partner_plan/)).toBeInTheDocument();
expect(screen.queryByTestId('plan-pricing-form')).toBeNull(); 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 () => { 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/ })); await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ }));
const user = userEvent.setup(); const user = userEvent.setup();
// base_name // base_name
@ -66,7 +67,7 @@ describe('PlanForm', () => {
// advanced_accounting_attributes.analytical_section // advanced_accounting_attributes.analytical_section
fireEvent.change(screen.getByLabelText(/app.admin.advanced_accounting_form.analytical_section/), { target: { value: '9B20A' } }); fireEvent.change(screen.getByLabelText(/app.admin.advanced_accounting_form.analytical_section/), { target: { value: '9B20A' } });
// send the form // 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(() => { await waitFor(() => {
const expected: Plan = { const expected: Plan = {
base_name: 'Test Plan', base_name: 'Test Plan',
@ -98,7 +99,7 @@ describe('PlanForm', () => {
test('render update PlanForm with partner', async () => { test('render update PlanForm with partner', async () => {
const plan = plans[1]; 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/ })); 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.getByLabelText(/app.admin.plan_form.name/)).toBeInTheDocument();
expect(screen.queryByLabelText(/app.admin.plan_form.transversal/)).toBeNull(); 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.getByLabelText(/app.admin.plan_form.notified_partner/)).toBeInTheDocument();
expect(screen.getByText(/app.admin.plan_form.alert_partner_notification/)).toBeInTheDocument(); expect(screen.getByText(/app.admin.plan_form.alert_partner_notification/)).toBeInTheDocument();
expect(screen.getByTestId('plan-pricing-form')).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 () => { 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/ })); await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ }));
fireEvent.click(screen.getByRole('switch', { name: /app.admin.plan_form.transversal/ })); fireEvent.click(screen.getByRole('switch', { name: /app.admin.plan_form.transversal/ }));
expect(screen.queryByRole('combobox', { name: /app.admin.plan_form.group/ })).toBeNull(); expect(screen.queryByRole('combobox', { name: /app.admin.plan_form.group/ })).toBeNull();
}); });
test('selecting partner plan shows partner selection', async () => { 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/ })); 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('switch', { name: /app.admin.plan_form.partner_plan/ }));
expect(screen.getByLabelText(/app.admin.plan_form.notified_partner/)); 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 () => { 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/ })); 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('switch', { name: /app.admin.plan_form.partner_plan/ }));
fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.new_user/ })); fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.new_user/ }));
@ -157,12 +158,12 @@ describe('PlanForm', () => {
test('update plan prices', async () => { test('update plan prices', async () => {
const plan = plans[1]; const plan = plans[1];
const machine = machines[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))); await waitFor(() => screen.getByLabelText(new RegExp(machine.name)));
// update machine price // update machine price
fireEvent.change(screen.getByLabelText(new RegExp(machine.name)), { target: { value: 42.42 } }); fireEvent.change(screen.getByLabelText(new RegExp(machine.name)), { target: { value: 42.42 } });
// send the form // 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(() => { await waitFor(() => {
const expected = { const expected = {
prices_attributes: expect.arrayContaining([{ prices_attributes: expect.arrayContaining([{

View File

@ -72,7 +72,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest
}.to_json, headers: default_headers }.to_json, headers: default_headers
end end
assert_equal 201, response.status assert_equal 201, response.status, response.body
assert_equal reservations_count + 1, Reservation.count assert_equal reservations_count + 1, Reservation.count
assert_equal invoices_count + 1, Invoice.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 # Check the id
availability = json_response(response.body) availability = json_response(response.body)
@ -141,7 +141,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest
}.to_json, headers: default_headers }.to_json, headers: default_headers
end end
assert_equal 422, response.status assert_equal 422, response.status, response.body
assert_match(/availability is restricted for subscribers/, response.body) assert_match(/availability is restricted for subscribers/, response.body)
assert_equal reservations_count, Reservation.count 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 # Check the id
availability = json_response(response.body) availability = json_response(response.body)
@ -187,27 +187,25 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest
slot = Availability.find(availability[:id]).slots.first slot = Availability.find(availability[:id]).slots.first
# book a reservation # book a reservation
VCR.use_cassette('reservations_create_for_restricted_slot_forced') do post '/api/local_payment/confirm_payment',
post '/api/local_payment/confirm_payment', params: {
params: { customer_id: @jdupont.id,
customer_id: @jdupont.id, items: [
items: [ {
{ reservation: {
reservation: { reservable_id: 2,
reservable_id: 2, reservable_type: 'Machine',
reservable_type: 'Machine', slots_reservations_attributes: [
slots_reservations_attributes: [ {
{ slot_id: slot.id
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 # Check the result
result = json_response(response.body) 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 # Check the id
availability = json_response(response.body) availability = json_response(response.body)
@ -287,7 +285,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest
}.to_json, headers: default_headers }.to_json, headers: default_headers
end end
assert_equal 201, response.status assert_equal 201, response.status, response.body
# Check the result # Check the result
result = json_response(response.body) result = json_response(response.body)

View File

@ -3,7 +3,7 @@
require 'test_helper' require 'test_helper'
# Test the service returning the availabilities for the given resources # Test the service returning the availabilities for the given resources
class AvailabilitiesServiceTest < ActiveSupport::TestCase class Availabilities::AvailabilitiesServiceTest < ActiveSupport::TestCase
setup do setup do
@no_subscription = User.find_by(username: 'jdupond') @no_subscription = User.find_by(username: 'jdupond')
@with_subscription = User.find_by(username: 'kdumas') @with_subscription = User.find_by(username: 'kdumas')

View 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

View 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

View File

@ -111,6 +111,11 @@ class ActiveSupport::TestCase
assert_not_nil actual, msg assert_not_nil actual, msg
assert_equal expected.to_date, actual.to_date, msg assert_equal expected.to_date, actual.to_date, msg
end end
def assert_datetimes_equal(expected, actual, msg = nil)
assert_not_nil actual, msg
assert_equal expected.iso8601, actual.iso8601, msg
end
end end
class ActionDispatch::IntegrationTest class ActionDispatch::IntegrationTest

View File

@ -5,7 +5,7 @@ http_interactions:
uri: https://api.stripe.com/v1/payment_methods uri: https://api.stripe.com/v1/payment_methods
body: body:
encoding: UTF-8 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: headers:
User-Agent: User-Agent:
- Stripe/v1 RubyBindings/5.29.0 - Stripe/v1 RubyBindings/5.29.0
@ -13,12 +13,14 @@ http_interactions:
- Bearer sk_test_testfaketestfaketestfake - Bearer sk_test_testfaketestfaketestfake
Content-Type: Content-Type:
- application/x-www-form-urlencoded - application/x-www-form-urlencoded
X-Stripe-Client-Telemetry:
- '{"last_request_metrics":{"request_id":"req_MQd4Z7i8cW9FYF","request_duration_ms":535}}'
Stripe-Version: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-Client-User-Agent: 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 - '{"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
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
Accept-Encoding: Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept: Accept:
@ -31,11 +33,11 @@ http_interactions:
Server: Server:
- nginx - nginx
Date: Date:
- Mon, 13 Sep 2021 11:24:07 GMT - Wed, 15 Mar 2023 11:51:50 GMT
Content-Type: Content-Type:
- application/json - application/json
Content-Length: Content-Length:
- '934' - '930'
Connection: Connection:
- keep-alive - keep-alive
Access-Control-Allow-Credentials: Access-Control-Allow-Credentials:
@ -50,19 +52,23 @@ http_interactions:
- '300' - '300'
Cache-Control: Cache-Control:
- no-cache, no-store - no-cache, no-store
Idempotency-Key:
- 8b19d06b-ed60-406d-8490-3d7062d47f67
Original-Request:
- req_jrCbj5YQwrn3m7
Request-Id: Request-Id:
- req_tlTlxEJC4LyAQv - req_jrCbj5YQwrn3m7
Stripe-Should-Retry:
- 'false'
Stripe-Version: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-C-Cost:
- '6'
Strict-Transport-Security: Strict-Transport-Security:
- max-age=31556926; includeSubDomains; preload - max-age=63072000; includeSubDomains; preload
body: body:
encoding: UTF-8 encoding: UTF-8
string: | string: |-
{ {
"id": "pm_1JZDGd2sOmf47Nz9LCckU76B", "id": "pm_1Mlsry2sOmf47Nz9g8twwVyn",
"object": "payment_method", "object": "payment_method",
"billing_details": { "billing_details": {
"address": { "address": {
@ -86,7 +92,7 @@ http_interactions:
}, },
"country": "US", "country": "US",
"exp_month": 4, "exp_month": 4,
"exp_year": 2022, "exp_year": 2024,
"fingerprint": "o52jybR7bnmNn6AT", "fingerprint": "o52jybR7bnmNn6AT",
"funding": "credit", "funding": "credit",
"generated_from": null, "generated_from": null,
@ -102,20 +108,19 @@ http_interactions:
}, },
"wallet": null "wallet": null
}, },
"created": 1631532247, "created": 1678881110,
"customer": null, "customer": null,
"livemode": false, "livemode": false,
"metadata": { "metadata": {},
},
"type": "card" "type": "card"
} }
recorded_at: Mon, 13 Sep 2021 11:24:07 GMT recorded_at: Wed, 15 Mar 2023 11:51:50 GMT
- request: - request:
method: post method: post
uri: https://api.stripe.com/v1/payment_intents uri: https://api.stripe.com/v1/payment_intents
body: body:
encoding: UTF-8 encoding: UTF-8
string: payment_method=pm_1JZDGd2sOmf47Nz9LCckU76B&amount=11500&currency=usd&confirmation_method=manual&confirm=true&customer=cus_8Di1wjdVktv5kt string: payment_method=pm_1Mlsry2sOmf47Nz9g8twwVyn&amount=11500&currency=usd&confirmation_method=manual&confirm=true&customer=cus_8Di1wjdVktv5kt
headers: headers:
User-Agent: User-Agent:
- Stripe/v1 RubyBindings/5.29.0 - Stripe/v1 RubyBindings/5.29.0
@ -124,13 +129,13 @@ http_interactions:
Content-Type: Content-Type:
- application/x-www-form-urlencoded - application/x-www-form-urlencoded
X-Stripe-Client-Telemetry: 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: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-Client-User-Agent: 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 - '{"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
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
Accept-Encoding: Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept: Accept:
@ -143,11 +148,11 @@ http_interactions:
Server: Server:
- nginx - nginx
Date: Date:
- Mon, 13 Sep 2021 11:24:09 GMT - Wed, 15 Mar 2023 11:51:52 GMT
Content-Type: Content-Type:
- application/json - application/json
Content-Length: Content-Length:
- '4263' - '4522'
Connection: Connection:
- keep-alive - keep-alive
Access-Control-Allow-Credentials: Access-Control-Allow-Credentials:
@ -162,25 +167,33 @@ http_interactions:
- '300' - '300'
Cache-Control: Cache-Control:
- no-cache, no-store - no-cache, no-store
Idempotency-Key:
- bceb9ff3-545c-48c9-9f9b-4654dd50641b
Original-Request:
- req_2QEtRciNfNblB1
Request-Id: Request-Id:
- req_4hAutJ5WkAA9ps - req_2QEtRciNfNblB1
Stripe-Should-Retry:
- 'false'
Stripe-Version: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-C-Cost:
- '10'
Strict-Transport-Security: Strict-Transport-Security:
- max-age=31556926; includeSubDomains; preload - max-age=63072000; includeSubDomains; preload
body: body:
encoding: UTF-8 encoding: UTF-8
string: | string: |-
{ {
"id": "pi_3JZDGd2sOmf47Nz91tgWkK3L", "id": "pi_3Mlsrz2sOmf47Nz90UCTYKFx",
"object": "payment_intent", "object": "payment_intent",
"amount": 11500, "amount": 11500,
"amount_capturable": 0, "amount_capturable": 0,
"amount_details": {
"tip": {}
},
"amount_received": 11500, "amount_received": 11500,
"application": null, "application": null,
"application_fee_amount": null, "application_fee_amount": null,
"automatic_payment_methods": null,
"canceled_at": null, "canceled_at": null,
"cancellation_reason": null, "cancellation_reason": null,
"capture_method": "automatic", "capture_method": "automatic",
@ -188,7 +201,7 @@ http_interactions:
"object": "list", "object": "list",
"data": [ "data": [
{ {
"id": "ch_3JZDGd2sOmf47Nz91HsbEa5U", "id": "ch_3Mlsrz2sOmf47Nz9004R5HME",
"object": "charge", "object": "charge",
"amount": 11500, "amount": 11500,
"amount_captured": 11500, "amount_captured": 11500,
@ -196,7 +209,7 @@ http_interactions:
"application": null, "application": null,
"application_fee": null, "application_fee": null,
"application_fee_amount": null, "application_fee_amount": null,
"balance_transaction": "txn_3JZDGd2sOmf47Nz91MEnox3F", "balance_transaction": "txn_3Mlsrz2sOmf47Nz90SLqRLy1",
"billing_details": { "billing_details": {
"address": { "address": {
"city": null, "city": null,
@ -212,34 +225,33 @@ http_interactions:
}, },
"calculated_statement_descriptor": "Stripe", "calculated_statement_descriptor": "Stripe",
"captured": true, "captured": true,
"created": 1631532248, "created": 1678881111,
"currency": "usd", "currency": "usd",
"customer": "cus_8Di1wjdVktv5kt", "customer": "cus_8Di1wjdVktv5kt",
"description": null, "description": null,
"destination": null, "destination": null,
"dispute": null, "dispute": null,
"disputed": false, "disputed": false,
"failure_balance_transaction": null,
"failure_code": null, "failure_code": null,
"failure_message": null, "failure_message": null,
"fraud_details": { "fraud_details": {},
},
"invoice": null, "invoice": null,
"livemode": false, "livemode": false,
"metadata": { "metadata": {},
},
"on_behalf_of": null, "on_behalf_of": null,
"order": null, "order": null,
"outcome": { "outcome": {
"network_status": "approved_by_network", "network_status": "approved_by_network",
"reason": null, "reason": null,
"risk_level": "normal", "risk_level": "normal",
"risk_score": 12, "risk_score": 29,
"seller_message": "Payment complete.", "seller_message": "Payment complete.",
"type": "authorized" "type": "authorized"
}, },
"paid": true, "paid": true,
"payment_intent": "pi_3JZDGd2sOmf47Nz91tgWkK3L", "payment_intent": "pi_3Mlsrz2sOmf47Nz90UCTYKFx",
"payment_method": "pm_1JZDGd2sOmf47Nz9LCckU76B", "payment_method": "pm_1Mlsry2sOmf47Nz9g8twwVyn",
"payment_method_details": { "payment_method_details": {
"card": { "card": {
"brand": "visa", "brand": "visa",
@ -250,11 +262,12 @@ http_interactions:
}, },
"country": "US", "country": "US",
"exp_month": 4, "exp_month": 4,
"exp_year": 2022, "exp_year": 2024,
"fingerprint": "o52jybR7bnmNn6AT", "fingerprint": "o52jybR7bnmNn6AT",
"funding": "credit", "funding": "credit",
"installments": null, "installments": null,
"last4": "4242", "last4": "4242",
"mandate": null,
"network": "visa", "network": "visa",
"three_d_secure": null, "three_d_secure": null,
"wallet": null "wallet": null
@ -263,16 +276,14 @@ http_interactions:
}, },
"receipt_email": null, "receipt_email": null,
"receipt_number": 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, "refunded": false,
"refunds": { "refunds": {
"object": "list", "object": "list",
"data": [ "data": [],
],
"has_more": false, "has_more": false,
"total_count": 0, "total_count": 0,
"url": "/v1/charges/ch_3JZDGd2sOmf47Nz91HsbEa5U/refunds" "url": "/v1/charges/ch_3Mlsrz2sOmf47Nz9004R5HME/refunds"
}, },
"review": null, "review": null,
"shipping": null, "shipping": null,
@ -287,25 +298,26 @@ http_interactions:
], ],
"has_more": false, "has_more": false,
"total_count": 1, "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", "confirmation_method": "manual",
"created": 1631532247, "created": 1678881111,
"currency": "usd", "currency": "usd",
"customer": "cus_8Di1wjdVktv5kt", "customer": "cus_8Di1wjdVktv5kt",
"description": null, "description": null,
"invoice": null, "invoice": null,
"last_payment_error": null, "last_payment_error": null,
"latest_charge": "ch_3Mlsrz2sOmf47Nz9004R5HME",
"livemode": false, "livemode": false,
"metadata": { "metadata": {},
},
"next_action": null, "next_action": null,
"on_behalf_of": null, "on_behalf_of": null,
"payment_method": "pm_1JZDGd2sOmf47Nz9LCckU76B", "payment_method": "pm_1Mlsry2sOmf47Nz9g8twwVyn",
"payment_method_options": { "payment_method_options": {
"card": { "card": {
"installments": null, "installments": null,
"mandate_options": null,
"network": null, "network": null,
"request_three_d_secure": "automatic" "request_three_d_secure": "automatic"
} }
@ -313,6 +325,7 @@ http_interactions:
"payment_method_types": [ "payment_method_types": [
"card" "card"
], ],
"processing": null,
"receipt_email": null, "receipt_email": null,
"review": null, "review": null,
"setup_future_usage": null, "setup_future_usage": null,
@ -324,13 +337,13 @@ http_interactions:
"transfer_data": null, "transfer_data": null,
"transfer_group": 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: - request:
method: post method: post
uri: https://api.stripe.com/v1/payment_intents/pi_3JZDGd2sOmf47Nz91tgWkK3L uri: https://api.stripe.com/v1/payment_intents/pi_3Mlsrz2sOmf47Nz90UCTYKFx
body: body:
encoding: UTF-8 encoding: UTF-8
string: description=Invoice+reference%3A+2109001%2FVL string: description=Invoice+reference%3A+2303007%2FVL
headers: headers:
User-Agent: User-Agent:
- Stripe/v1 RubyBindings/5.29.0 - Stripe/v1 RubyBindings/5.29.0
@ -339,13 +352,13 @@ http_interactions:
Content-Type: Content-Type:
- application/x-www-form-urlencoded - application/x-www-form-urlencoded
X-Stripe-Client-Telemetry: 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: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-Client-User-Agent: 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 - '{"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
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
Accept-Encoding: Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept: Accept:
@ -358,11 +371,11 @@ http_interactions:
Server: Server:
- nginx - nginx
Date: Date:
- Mon, 13 Sep 2021 11:24:10 GMT - Wed, 15 Mar 2023 11:51:52 GMT
Content-Type: Content-Type:
- application/json - application/json
Content-Length: Content-Length:
- '4290' - '4549'
Connection: Connection:
- keep-alive - keep-alive
Access-Control-Allow-Credentials: Access-Control-Allow-Credentials:
@ -377,25 +390,33 @@ http_interactions:
- '300' - '300'
Cache-Control: Cache-Control:
- no-cache, no-store - no-cache, no-store
Idempotency-Key:
- 51790c68-63e7-4b6e-96b1-4b6eef30b42b
Original-Request:
- req_Ms4raQ174jWmPK
Request-Id: Request-Id:
- req_ysWG3JfyCp5xVD - req_Ms4raQ174jWmPK
Stripe-Should-Retry:
- 'false'
Stripe-Version: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-C-Cost:
- '0'
Strict-Transport-Security: Strict-Transport-Security:
- max-age=31556926; includeSubDomains; preload - max-age=63072000; includeSubDomains; preload
body: body:
encoding: UTF-8 encoding: UTF-8
string: | string: |-
{ {
"id": "pi_3JZDGd2sOmf47Nz91tgWkK3L", "id": "pi_3Mlsrz2sOmf47Nz90UCTYKFx",
"object": "payment_intent", "object": "payment_intent",
"amount": 11500, "amount": 11500,
"amount_capturable": 0, "amount_capturable": 0,
"amount_details": {
"tip": {}
},
"amount_received": 11500, "amount_received": 11500,
"application": null, "application": null,
"application_fee_amount": null, "application_fee_amount": null,
"automatic_payment_methods": null,
"canceled_at": null, "canceled_at": null,
"cancellation_reason": null, "cancellation_reason": null,
"capture_method": "automatic", "capture_method": "automatic",
@ -403,7 +424,7 @@ http_interactions:
"object": "list", "object": "list",
"data": [ "data": [
{ {
"id": "ch_3JZDGd2sOmf47Nz91HsbEa5U", "id": "ch_3Mlsrz2sOmf47Nz9004R5HME",
"object": "charge", "object": "charge",
"amount": 11500, "amount": 11500,
"amount_captured": 11500, "amount_captured": 11500,
@ -411,7 +432,7 @@ http_interactions:
"application": null, "application": null,
"application_fee": null, "application_fee": null,
"application_fee_amount": null, "application_fee_amount": null,
"balance_transaction": "txn_3JZDGd2sOmf47Nz91MEnox3F", "balance_transaction": "txn_3Mlsrz2sOmf47Nz90SLqRLy1",
"billing_details": { "billing_details": {
"address": { "address": {
"city": null, "city": null,
@ -427,34 +448,33 @@ http_interactions:
}, },
"calculated_statement_descriptor": "Stripe", "calculated_statement_descriptor": "Stripe",
"captured": true, "captured": true,
"created": 1631532248, "created": 1678881111,
"currency": "usd", "currency": "usd",
"customer": "cus_8Di1wjdVktv5kt", "customer": "cus_8Di1wjdVktv5kt",
"description": null, "description": null,
"destination": null, "destination": null,
"dispute": null, "dispute": null,
"disputed": false, "disputed": false,
"failure_balance_transaction": null,
"failure_code": null, "failure_code": null,
"failure_message": null, "failure_message": null,
"fraud_details": { "fraud_details": {},
},
"invoice": null, "invoice": null,
"livemode": false, "livemode": false,
"metadata": { "metadata": {},
},
"on_behalf_of": null, "on_behalf_of": null,
"order": null, "order": null,
"outcome": { "outcome": {
"network_status": "approved_by_network", "network_status": "approved_by_network",
"reason": null, "reason": null,
"risk_level": "normal", "risk_level": "normal",
"risk_score": 12, "risk_score": 29,
"seller_message": "Payment complete.", "seller_message": "Payment complete.",
"type": "authorized" "type": "authorized"
}, },
"paid": true, "paid": true,
"payment_intent": "pi_3JZDGd2sOmf47Nz91tgWkK3L", "payment_intent": "pi_3Mlsrz2sOmf47Nz90UCTYKFx",
"payment_method": "pm_1JZDGd2sOmf47Nz9LCckU76B", "payment_method": "pm_1Mlsry2sOmf47Nz9g8twwVyn",
"payment_method_details": { "payment_method_details": {
"card": { "card": {
"brand": "visa", "brand": "visa",
@ -465,11 +485,12 @@ http_interactions:
}, },
"country": "US", "country": "US",
"exp_month": 4, "exp_month": 4,
"exp_year": 2022, "exp_year": 2024,
"fingerprint": "o52jybR7bnmNn6AT", "fingerprint": "o52jybR7bnmNn6AT",
"funding": "credit", "funding": "credit",
"installments": null, "installments": null,
"last4": "4242", "last4": "4242",
"mandate": null,
"network": "visa", "network": "visa",
"three_d_secure": null, "three_d_secure": null,
"wallet": null "wallet": null
@ -478,16 +499,14 @@ http_interactions:
}, },
"receipt_email": null, "receipt_email": null,
"receipt_number": 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, "refunded": false,
"refunds": { "refunds": {
"object": "list", "object": "list",
"data": [ "data": [],
],
"has_more": false, "has_more": false,
"total_count": 0, "total_count": 0,
"url": "/v1/charges/ch_3JZDGd2sOmf47Nz91HsbEa5U/refunds" "url": "/v1/charges/ch_3Mlsrz2sOmf47Nz9004R5HME/refunds"
}, },
"review": null, "review": null,
"shipping": null, "shipping": null,
@ -502,25 +521,26 @@ http_interactions:
], ],
"has_more": false, "has_more": false,
"total_count": 1, "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", "confirmation_method": "manual",
"created": 1631532247, "created": 1678881111,
"currency": "usd", "currency": "usd",
"customer": "cus_8Di1wjdVktv5kt", "customer": "cus_8Di1wjdVktv5kt",
"description": "Invoice reference: 2109001/VL", "description": "Invoice reference: 2303007/VL",
"invoice": null, "invoice": null,
"last_payment_error": null, "last_payment_error": null,
"latest_charge": "ch_3Mlsrz2sOmf47Nz9004R5HME",
"livemode": false, "livemode": false,
"metadata": { "metadata": {},
},
"next_action": null, "next_action": null,
"on_behalf_of": null, "on_behalf_of": null,
"payment_method": "pm_1JZDGd2sOmf47Nz9LCckU76B", "payment_method": "pm_1Mlsry2sOmf47Nz9g8twwVyn",
"payment_method_options": { "payment_method_options": {
"card": { "card": {
"installments": null, "installments": null,
"mandate_options": null,
"network": null, "network": null,
"request_three_d_secure": "automatic" "request_three_d_secure": "automatic"
} }
@ -528,6 +548,7 @@ http_interactions:
"payment_method_types": [ "payment_method_types": [
"card" "card"
], ],
"processing": null,
"receipt_email": null, "receipt_email": null,
"review": null, "review": null,
"setup_future_usage": null, "setup_future_usage": null,
@ -539,5 +560,5 @@ http_interactions:
"transfer_data": null, "transfer_data": null,
"transfer_group": 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 recorded_with: VCR 6.0.0

View File

@ -5,7 +5,7 @@ http_interactions:
uri: https://api.stripe.com/v1/payment_methods uri: https://api.stripe.com/v1/payment_methods
body: body:
encoding: UTF-8 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: headers:
User-Agent: User-Agent:
- Stripe/v1 RubyBindings/5.29.0 - Stripe/v1 RubyBindings/5.29.0
@ -13,14 +13,12 @@ http_interactions:
- Bearer sk_test_testfaketestfaketestfake - Bearer sk_test_testfaketestfaketestfake
Content-Type: Content-Type:
- application/x-www-form-urlencoded - application/x-www-form-urlencoded
X-Stripe-Client-Telemetry:
- '{"last_request_metrics":{"request_id":"req_ysWG3JfyCp5xVD","request_duration_ms":520}}'
Stripe-Version: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-Client-User-Agent: 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 - '{"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
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
Accept-Encoding: Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept: Accept:
@ -33,11 +31,11 @@ http_interactions:
Server: Server:
- nginx - nginx
Date: Date:
- Mon, 13 Sep 2021 11:24:10 GMT - Wed, 15 Mar 2023 11:51:20 GMT
Content-Type: Content-Type:
- application/json - application/json
Content-Length: Content-Length:
- '934' - '930'
Connection: Connection:
- keep-alive - keep-alive
Access-Control-Allow-Credentials: Access-Control-Allow-Credentials:
@ -52,19 +50,23 @@ http_interactions:
- '300' - '300'
Cache-Control: Cache-Control:
- no-cache, no-store - no-cache, no-store
Idempotency-Key:
- 6a183d91-3220-4ad4-913c-f169a75aa488
Original-Request:
- req_LQKka6p7rniNKT
Request-Id: Request-Id:
- req_7RNGSU2vySHdHz - req_LQKka6p7rniNKT
Stripe-Should-Retry:
- 'false'
Stripe-Version: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-C-Cost:
- '6'
Strict-Transport-Security: Strict-Transport-Security:
- max-age=31556926; includeSubDomains; preload - max-age=63072000; includeSubDomains; preload
body: body:
encoding: UTF-8 encoding: UTF-8
string: | string: |-
{ {
"id": "pm_1JZDGg2sOmf47Nz9pfmMaPtb", "id": "pm_1MlsrU2sOmf47Nz9voyfBlTb",
"object": "payment_method", "object": "payment_method",
"billing_details": { "billing_details": {
"address": { "address": {
@ -88,7 +90,7 @@ http_interactions:
}, },
"country": "US", "country": "US",
"exp_month": 4, "exp_month": 4,
"exp_year": 2022, "exp_year": 2024,
"fingerprint": "o52jybR7bnmNn6AT", "fingerprint": "o52jybR7bnmNn6AT",
"funding": "credit", "funding": "credit",
"generated_from": null, "generated_from": null,
@ -104,227 +106,11 @@ http_interactions:
}, },
"wallet": null "wallet": null
}, },
"created": 1631532250, "created": 1678881080,
"customer": null, "customer": null,
"livemode": false, "livemode": false,
"metadata": { "metadata": {},
},
"type": "card" "type": "card"
} }
recorded_at: Mon, 13 Sep 2021 11:24:10 GMT recorded_at: Wed, 15 Mar 2023 11:51:20 GMT
- request:
method: post
uri: https://api.stripe.com/v1/payment_intents
body:
encoding: UTF-8
string: payment_method=pm_1JZDGg2sOmf47Nz9pfmMaPtb&amount=4200&currency=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_with: VCR 6.0.0 recorded_with: VCR 6.0.0

View File

@ -5,7 +5,7 @@ http_interactions:
uri: https://api.stripe.com/v1/payment_methods uri: https://api.stripe.com/v1/payment_methods
body: body:
encoding: UTF-8 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: headers:
User-Agent: User-Agent:
- Stripe/v1 RubyBindings/5.29.0 - Stripe/v1 RubyBindings/5.29.0
@ -13,14 +13,12 @@ http_interactions:
- Bearer sk_test_testfaketestfaketestfake - Bearer sk_test_testfaketestfaketestfake
Content-Type: Content-Type:
- application/x-www-form-urlencoded - application/x-www-form-urlencoded
X-Stripe-Client-Telemetry:
- '{"last_request_metrics":{"request_id":"req_NgGOJxEFd8THv8","request_duration_ms":1772}}'
Stripe-Version: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-Client-User-Agent: 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 - '{"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
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
Accept-Encoding: Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept: Accept:
@ -33,11 +31,11 @@ http_interactions:
Server: Server:
- nginx - nginx
Date: Date:
- Mon, 13 Sep 2021 11:24:13 GMT - Wed, 15 Mar 2023 11:51:46 GMT
Content-Type: Content-Type:
- application/json - application/json
Content-Length: Content-Length:
- '934' - '930'
Connection: Connection:
- keep-alive - keep-alive
Access-Control-Allow-Credentials: Access-Control-Allow-Credentials:
@ -52,19 +50,23 @@ http_interactions:
- '300' - '300'
Cache-Control: Cache-Control:
- no-cache, no-store - no-cache, no-store
Idempotency-Key:
- 4a6e0351-64bd-4ede-a018-7c03e320c3f5
Original-Request:
- req_njTGdMZNpa6wpG
Request-Id: Request-Id:
- req_px5zsAdlzgwwSe - req_njTGdMZNpa6wpG
Stripe-Should-Retry:
- 'false'
Stripe-Version: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-C-Cost:
- '6'
Strict-Transport-Security: Strict-Transport-Security:
- max-age=31556926; includeSubDomains; preload - max-age=63072000; includeSubDomains; preload
body: body:
encoding: UTF-8 encoding: UTF-8
string: | string: |-
{ {
"id": "pm_1JZDGj2sOmf47Nz9S8jhZkFt", "id": "pm_1Mlsru2sOmf47Nz9s3VNHXYt",
"object": "payment_method", "object": "payment_method",
"billing_details": { "billing_details": {
"address": { "address": {
@ -88,7 +90,7 @@ http_interactions:
}, },
"country": "US", "country": "US",
"exp_month": 4, "exp_month": 4,
"exp_year": 2022, "exp_year": 2024,
"fingerprint": "o52jybR7bnmNn6AT", "fingerprint": "o52jybR7bnmNn6AT",
"funding": "credit", "funding": "credit",
"generated_from": null, "generated_from": null,
@ -104,20 +106,19 @@ http_interactions:
}, },
"wallet": null "wallet": null
}, },
"created": 1631532253, "created": 1678881106,
"customer": null, "customer": null,
"livemode": false, "livemode": false,
"metadata": { "metadata": {},
},
"type": "card" "type": "card"
} }
recorded_at: Mon, 13 Sep 2021 11:24:13 GMT recorded_at: Wed, 15 Mar 2023 11:51:46 GMT
- request: - request:
method: post method: post
uri: https://api.stripe.com/v1/payment_intents uri: https://api.stripe.com/v1/payment_intents
body: body:
encoding: UTF-8 encoding: UTF-8
string: payment_method=pm_1JZDGj2sOmf47Nz9S8jhZkFt&amount=1500&currency=usd&confirmation_method=manual&confirm=true&customer=cus_8CzHcwBJtlA3IL string: payment_method=pm_1Mlsru2sOmf47Nz9s3VNHXYt&amount=1500&currency=usd&confirmation_method=manual&confirm=true&customer=cus_8CzHcwBJtlA3IL
headers: headers:
User-Agent: User-Agent:
- Stripe/v1 RubyBindings/5.29.0 - Stripe/v1 RubyBindings/5.29.0
@ -126,13 +127,13 @@ http_interactions:
Content-Type: Content-Type:
- application/x-www-form-urlencoded - application/x-www-form-urlencoded
X-Stripe-Client-Telemetry: 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: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-Client-User-Agent: 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 - '{"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
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
Accept-Encoding: Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept: Accept:
@ -145,11 +146,11 @@ http_interactions:
Server: Server:
- nginx - nginx
Date: Date:
- Mon, 13 Sep 2021 11:24:15 GMT - Wed, 15 Mar 2023 11:51:48 GMT
Content-Type: Content-Type:
- application/json - application/json
Content-Length: Content-Length:
- '4259' - '4517'
Connection: Connection:
- keep-alive - keep-alive
Access-Control-Allow-Credentials: Access-Control-Allow-Credentials:
@ -164,25 +165,33 @@ http_interactions:
- '300' - '300'
Cache-Control: Cache-Control:
- no-cache, no-store - no-cache, no-store
Idempotency-Key:
- f50a36a7-97b5-4d67-9c77-43b5cbddccbe
Original-Request:
- req_EocoIWBH6tucS4
Request-Id: Request-Id:
- req_EMDO1Z1Uux0kJb - req_EocoIWBH6tucS4
Stripe-Should-Retry:
- 'false'
Stripe-Version: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-C-Cost:
- '10'
Strict-Transport-Security: Strict-Transport-Security:
- max-age=31556926; includeSubDomains; preload - max-age=63072000; includeSubDomains; preload
body: body:
encoding: UTF-8 encoding: UTF-8
string: | string: |-
{ {
"id": "pi_3JZDGj2sOmf47Nz90n2aOsuM", "id": "pi_3Mlsrv2sOmf47Nz91hRe49rq",
"object": "payment_intent", "object": "payment_intent",
"amount": 1500, "amount": 1500,
"amount_capturable": 0, "amount_capturable": 0,
"amount_details": {
"tip": {}
},
"amount_received": 1500, "amount_received": 1500,
"application": null, "application": null,
"application_fee_amount": null, "application_fee_amount": null,
"automatic_payment_methods": null,
"canceled_at": null, "canceled_at": null,
"cancellation_reason": null, "cancellation_reason": null,
"capture_method": "automatic", "capture_method": "automatic",
@ -190,7 +199,7 @@ http_interactions:
"object": "list", "object": "list",
"data": [ "data": [
{ {
"id": "ch_3JZDGj2sOmf47Nz90qQzwGqL", "id": "ch_3Mlsrv2sOmf47Nz91760imzX",
"object": "charge", "object": "charge",
"amount": 1500, "amount": 1500,
"amount_captured": 1500, "amount_captured": 1500,
@ -198,7 +207,7 @@ http_interactions:
"application": null, "application": null,
"application_fee": null, "application_fee": null,
"application_fee_amount": null, "application_fee_amount": null,
"balance_transaction": "txn_3JZDGj2sOmf47Nz90fOwfgv5", "balance_transaction": "txn_3Mlsrv2sOmf47Nz915VOM7IM",
"billing_details": { "billing_details": {
"address": { "address": {
"city": null, "city": null,
@ -214,34 +223,33 @@ http_interactions:
}, },
"calculated_statement_descriptor": "Stripe", "calculated_statement_descriptor": "Stripe",
"captured": true, "captured": true,
"created": 1631532254, "created": 1678881107,
"currency": "usd", "currency": "usd",
"customer": "cus_8CzHcwBJtlA3IL", "customer": "cus_8CzHcwBJtlA3IL",
"description": null, "description": null,
"destination": null, "destination": null,
"dispute": null, "dispute": null,
"disputed": false, "disputed": false,
"failure_balance_transaction": null,
"failure_code": null, "failure_code": null,
"failure_message": null, "failure_message": null,
"fraud_details": { "fraud_details": {},
},
"invoice": null, "invoice": null,
"livemode": false, "livemode": false,
"metadata": { "metadata": {},
},
"on_behalf_of": null, "on_behalf_of": null,
"order": null, "order": null,
"outcome": { "outcome": {
"network_status": "approved_by_network", "network_status": "approved_by_network",
"reason": null, "reason": null,
"risk_level": "normal", "risk_level": "normal",
"risk_score": 22, "risk_score": 7,
"seller_message": "Payment complete.", "seller_message": "Payment complete.",
"type": "authorized" "type": "authorized"
}, },
"paid": true, "paid": true,
"payment_intent": "pi_3JZDGj2sOmf47Nz90n2aOsuM", "payment_intent": "pi_3Mlsrv2sOmf47Nz91hRe49rq",
"payment_method": "pm_1JZDGj2sOmf47Nz9S8jhZkFt", "payment_method": "pm_1Mlsru2sOmf47Nz9s3VNHXYt",
"payment_method_details": { "payment_method_details": {
"card": { "card": {
"brand": "visa", "brand": "visa",
@ -252,11 +260,12 @@ http_interactions:
}, },
"country": "US", "country": "US",
"exp_month": 4, "exp_month": 4,
"exp_year": 2022, "exp_year": 2024,
"fingerprint": "o52jybR7bnmNn6AT", "fingerprint": "o52jybR7bnmNn6AT",
"funding": "credit", "funding": "credit",
"installments": null, "installments": null,
"last4": "4242", "last4": "4242",
"mandate": null,
"network": "visa", "network": "visa",
"three_d_secure": null, "three_d_secure": null,
"wallet": null "wallet": null
@ -265,16 +274,14 @@ http_interactions:
}, },
"receipt_email": null, "receipt_email": null,
"receipt_number": 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, "refunded": false,
"refunds": { "refunds": {
"object": "list", "object": "list",
"data": [ "data": [],
],
"has_more": false, "has_more": false,
"total_count": 0, "total_count": 0,
"url": "/v1/charges/ch_3JZDGj2sOmf47Nz90qQzwGqL/refunds" "url": "/v1/charges/ch_3Mlsrv2sOmf47Nz91760imzX/refunds"
}, },
"review": null, "review": null,
"shipping": null, "shipping": null,
@ -289,25 +296,26 @@ http_interactions:
], ],
"has_more": false, "has_more": false,
"total_count": 1, "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", "confirmation_method": "manual",
"created": 1631532253, "created": 1678881107,
"currency": "usd", "currency": "usd",
"customer": "cus_8CzHcwBJtlA3IL", "customer": "cus_8CzHcwBJtlA3IL",
"description": null, "description": null,
"invoice": null, "invoice": null,
"last_payment_error": null, "last_payment_error": null,
"latest_charge": "ch_3Mlsrv2sOmf47Nz91760imzX",
"livemode": false, "livemode": false,
"metadata": { "metadata": {},
},
"next_action": null, "next_action": null,
"on_behalf_of": null, "on_behalf_of": null,
"payment_method": "pm_1JZDGj2sOmf47Nz9S8jhZkFt", "payment_method": "pm_1Mlsru2sOmf47Nz9s3VNHXYt",
"payment_method_options": { "payment_method_options": {
"card": { "card": {
"installments": null, "installments": null,
"mandate_options": null,
"network": null, "network": null,
"request_three_d_secure": "automatic" "request_three_d_secure": "automatic"
} }
@ -315,6 +323,7 @@ http_interactions:
"payment_method_types": [ "payment_method_types": [
"card" "card"
], ],
"processing": null,
"receipt_email": null, "receipt_email": null,
"review": null, "review": null,
"setup_future_usage": null, "setup_future_usage": null,
@ -326,13 +335,13 @@ http_interactions:
"transfer_data": null, "transfer_data": null,
"transfer_group": 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: - request:
method: post method: post
uri: https://api.stripe.com/v1/payment_intents/pi_3JZDGj2sOmf47Nz90n2aOsuM uri: https://api.stripe.com/v1/payment_intents/pi_3Mlsrv2sOmf47Nz91hRe49rq
body: body:
encoding: UTF-8 encoding: UTF-8
string: description=Invoice+reference%3A+2109001%2FVL string: description=Invoice+reference%3A+2303007%2FVL
headers: headers:
User-Agent: User-Agent:
- Stripe/v1 RubyBindings/5.29.0 - Stripe/v1 RubyBindings/5.29.0
@ -341,13 +350,13 @@ http_interactions:
Content-Type: Content-Type:
- application/x-www-form-urlencoded - application/x-www-form-urlencoded
X-Stripe-Client-Telemetry: 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: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-Client-User-Agent: 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 - '{"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
MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld
PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}'
Accept-Encoding: Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept: Accept:
@ -360,11 +369,11 @@ http_interactions:
Server: Server:
- nginx - nginx
Date: Date:
- Mon, 13 Sep 2021 11:24:15 GMT - Wed, 15 Mar 2023 11:51:49 GMT
Content-Type: Content-Type:
- application/json - application/json
Content-Length: Content-Length:
- '4286' - '4544'
Connection: Connection:
- keep-alive - keep-alive
Access-Control-Allow-Credentials: Access-Control-Allow-Credentials:
@ -379,25 +388,33 @@ http_interactions:
- '300' - '300'
Cache-Control: Cache-Control:
- no-cache, no-store - no-cache, no-store
Idempotency-Key:
- 75c78d8d-9df1-4bb7-af1b-7de3aeaa7056
Original-Request:
- req_MQd4Z7i8cW9FYF
Request-Id: Request-Id:
- req_Xmih0ndHQjzde4 - req_MQd4Z7i8cW9FYF
Stripe-Should-Retry:
- 'false'
Stripe-Version: Stripe-Version:
- '2019-08-14' - '2019-08-14'
X-Stripe-C-Cost:
- '0'
Strict-Transport-Security: Strict-Transport-Security:
- max-age=31556926; includeSubDomains; preload - max-age=63072000; includeSubDomains; preload
body: body:
encoding: UTF-8 encoding: UTF-8
string: | string: |-
{ {
"id": "pi_3JZDGj2sOmf47Nz90n2aOsuM", "id": "pi_3Mlsrv2sOmf47Nz91hRe49rq",
"object": "payment_intent", "object": "payment_intent",
"amount": 1500, "amount": 1500,
"amount_capturable": 0, "amount_capturable": 0,
"amount_details": {
"tip": {}
},
"amount_received": 1500, "amount_received": 1500,
"application": null, "application": null,
"application_fee_amount": null, "application_fee_amount": null,
"automatic_payment_methods": null,
"canceled_at": null, "canceled_at": null,
"cancellation_reason": null, "cancellation_reason": null,
"capture_method": "automatic", "capture_method": "automatic",
@ -405,7 +422,7 @@ http_interactions:
"object": "list", "object": "list",
"data": [ "data": [
{ {
"id": "ch_3JZDGj2sOmf47Nz90qQzwGqL", "id": "ch_3Mlsrv2sOmf47Nz91760imzX",
"object": "charge", "object": "charge",
"amount": 1500, "amount": 1500,
"amount_captured": 1500, "amount_captured": 1500,
@ -413,7 +430,7 @@ http_interactions:
"application": null, "application": null,
"application_fee": null, "application_fee": null,
"application_fee_amount": null, "application_fee_amount": null,
"balance_transaction": "txn_3JZDGj2sOmf47Nz90fOwfgv5", "balance_transaction": "txn_3Mlsrv2sOmf47Nz915VOM7IM",
"billing_details": { "billing_details": {
"address": { "address": {
"city": null, "city": null,
@ -429,34 +446,33 @@ http_interactions:
}, },
"calculated_statement_descriptor": "Stripe", "calculated_statement_descriptor": "Stripe",
"captured": true, "captured": true,
"created": 1631532254, "created": 1678881107,
"currency": "usd", "currency": "usd",
"customer": "cus_8CzHcwBJtlA3IL", "customer": "cus_8CzHcwBJtlA3IL",
"description": null, "description": null,
"destination": null, "destination": null,
"dispute": null, "dispute": null,
"disputed": false, "disputed": false,
"failure_balance_transaction": null,
"failure_code": null, "failure_code": null,
"failure_message": null, "failure_message": null,
"fraud_details": { "fraud_details": {},
},
"invoice": null, "invoice": null,
"livemode": false, "livemode": false,
"metadata": { "metadata": {},
},
"on_behalf_of": null, "on_behalf_of": null,
"order": null, "order": null,
"outcome": { "outcome": {
"network_status": "approved_by_network", "network_status": "approved_by_network",
"reason": null, "reason": null,
"risk_level": "normal", "risk_level": "normal",
"risk_score": 22, "risk_score": 7,
"seller_message": "Payment complete.", "seller_message": "Payment complete.",
"type": "authorized" "type": "authorized"
}, },
"paid": true, "paid": true,
"payment_intent": "pi_3JZDGj2sOmf47Nz90n2aOsuM", "payment_intent": "pi_3Mlsrv2sOmf47Nz91hRe49rq",
"payment_method": "pm_1JZDGj2sOmf47Nz9S8jhZkFt", "payment_method": "pm_1Mlsru2sOmf47Nz9s3VNHXYt",
"payment_method_details": { "payment_method_details": {
"card": { "card": {
"brand": "visa", "brand": "visa",
@ -467,11 +483,12 @@ http_interactions:
}, },
"country": "US", "country": "US",
"exp_month": 4, "exp_month": 4,
"exp_year": 2022, "exp_year": 2024,
"fingerprint": "o52jybR7bnmNn6AT", "fingerprint": "o52jybR7bnmNn6AT",
"funding": "credit", "funding": "credit",
"installments": null, "installments": null,
"last4": "4242", "last4": "4242",
"mandate": null,
"network": "visa", "network": "visa",
"three_d_secure": null, "three_d_secure": null,
"wallet": null "wallet": null
@ -480,16 +497,14 @@ http_interactions:
}, },
"receipt_email": null, "receipt_email": null,
"receipt_number": 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, "refunded": false,
"refunds": { "refunds": {
"object": "list", "object": "list",
"data": [ "data": [],
],
"has_more": false, "has_more": false,
"total_count": 0, "total_count": 0,
"url": "/v1/charges/ch_3JZDGj2sOmf47Nz90qQzwGqL/refunds" "url": "/v1/charges/ch_3Mlsrv2sOmf47Nz91760imzX/refunds"
}, },
"review": null, "review": null,
"shipping": null, "shipping": null,
@ -504,25 +519,26 @@ http_interactions:
], ],
"has_more": false, "has_more": false,
"total_count": 1, "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", "confirmation_method": "manual",
"created": 1631532253, "created": 1678881107,
"currency": "usd", "currency": "usd",
"customer": "cus_8CzHcwBJtlA3IL", "customer": "cus_8CzHcwBJtlA3IL",
"description": "Invoice reference: 2109001/VL", "description": "Invoice reference: 2303007/VL",
"invoice": null, "invoice": null,
"last_payment_error": null, "last_payment_error": null,
"latest_charge": "ch_3Mlsrv2sOmf47Nz91760imzX",
"livemode": false, "livemode": false,
"metadata": { "metadata": {},
},
"next_action": null, "next_action": null,
"on_behalf_of": null, "on_behalf_of": null,
"payment_method": "pm_1JZDGj2sOmf47Nz9S8jhZkFt", "payment_method": "pm_1Mlsru2sOmf47Nz9s3VNHXYt",
"payment_method_options": { "payment_method_options": {
"card": { "card": {
"installments": null, "installments": null,
"mandate_options": null,
"network": null, "network": null,
"request_three_d_secure": "automatic" "request_three_d_secure": "automatic"
} }
@ -530,6 +546,7 @@ http_interactions:
"payment_method_types": [ "payment_method_types": [
"card" "card"
], ],
"processing": null,
"receipt_email": null, "receipt_email": null,
"review": null, "review": null,
"setup_future_usage": null, "setup_future_usage": null,
@ -541,5 +558,5 @@ http_interactions:
"transfer_data": null, "transfer_data": null,
"transfer_group": 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 recorded_with: VCR 6.0.0

View File

@ -4626,15 +4626,11 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2" lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0: 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.30001397" version "1.0.30001460"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001397.tgz" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz"
integrity sha512-SW9N2TbCdLf0eiNDRrrQXx2sOkaakNZbCjgNpPyMJJbiOrU5QzMIrXOVMRM1myBXTD5iTkdrtU/EguCrBocHlA== 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: chalk@4.1.1:
version "4.1.1" version "4.1.1"
@ -8965,10 +8961,10 @@ react-dom@^17.0.2:
object-assign "^4.1.1" object-assign "^4.1.1"
scheduler "^0.20.2" scheduler "^0.20.2"
react-hook-form@^7.30.0: react-hook-form@~7.31.3:
version "7.30.0" version "7.31.3"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.30.0.tgz#c9e2fd54d3627e43bd94bf38ef549df2e80c1371" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.31.3.tgz#b61bafb9a7435f91695351a7a9f714d8c4df0121"
integrity sha512-DzjiM6o2vtDGNMB9I4yCqW8J21P314SboNG1O0obROkbg7KVS0I7bMtwSdKyapnCPjHgnxc3L7E5PEdISeEUcQ== integrity sha512-NVZdCWViIWXXXlQ3jxVQH0NuNfwPf8A/0KvuCxrM9qxtP1qYosfR2ZudarziFrVOC7eTUbWbm1T4OyYCwv9oSQ==
react-i18next@^11.15.6: react-i18next@^11.15.6:
version "11.15.6" version "11.15.6"