diff --git a/CHANGELOG.md b/CHANGELOG.md index 3296b7405..0c1a63ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog Fab-manager +## v5.9.0 2023 March 20 + +- Ability to restrict machine reservations per plan +- Ability to restrict machine availabilities per plan +- Ability to configure a prior period for each reservation type to prevent booking (#440) +- Admins cannot select the date when creating a refund invoice anymore +- Fix a bug: JS date is initalialized 1 day before in negative timezones (#445) +- Fix a bug: user's profile field gender is now marked as required +- Fix a bug: logical sequence of invoices references is broken, when using the store module or the payments schedules +- Fix a bug: refund invoices may generate duplicates in invoices references +- Fix a security issue: updated webpack to 5.76.0 to fix [CVE-2023-28154](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28154) +- [TODO DEPLOY] `rails db:seed` +- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet` + ## v5.8.2 2023 March 13 - Improved upgrade script diff --git a/app/controllers/api/invoices_controller.rb b/app/controllers/api/invoices_controller.rb index e24225c1d..c03ac91e9 100644 --- a/app/controllers/api/invoices_controller.rb +++ b/app/controllers/api/invoices_controller.rb @@ -63,7 +63,7 @@ class API::InvoicesController < API::ApiController private def avoir_params - params.require(:avoir).permit(:invoice_id, :avoir_date, :payment_method, :subscription_to_expire, :description, + params.require(:avoir).permit(:invoice_id, :payment_method, :subscription_to_expire, :description, invoice_items_ids: []) end diff --git a/app/controllers/api/local_payment_controller.rb b/app/controllers/api/local_payment_controller.rb index 126510dd4..a5c18200f 100644 --- a/app/controllers/api/local_payment_controller.rb +++ b/app/controllers/api/local_payment_controller.rb @@ -8,6 +8,8 @@ class API::LocalPaymentController < API::PaymentsController authorize LocalPaymentContext.new(cart, price[:amount]) + render json: cart.errors, status: :unprocessable_entity and return unless cart.valid? + render on_payment_success(nil, nil, cart) end diff --git a/app/controllers/api/payzen_controller.rb b/app/controllers/api/payzen_controller.rb index 69fbaa70d..14eaf310b 100644 --- a/app/controllers/api/payzen_controller.rb +++ b/app/controllers/api/payzen_controller.rb @@ -55,7 +55,7 @@ class API::PayzenController < API::PaymentsController def check_cart cart = shopping_cart - render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid? + render json: cart.errors, status: :unprocessable_entity and return unless cart.valid? render json: { cart: 'ok' }, status: :ok end diff --git a/app/controllers/api/plans_controller.rb b/app/controllers/api/plans_controller.rb index f5895b07d..52d786cd5 100644 --- a/app/controllers/api/plans_controller.rb +++ b/app/controllers/api/plans_controller.rb @@ -80,11 +80,13 @@ class API::PlansController < API::ApiController end @parameters = @parameters.require(:plan) - .permit(:base_name, :type, :group_id, :amount, :interval, :interval_count, :is_rolling, + .permit(:base_name, :type, :group_id, :amount, :interval, :interval_count, :is_rolling, :limiting, :training_credit_nb, :ui_weight, :disabled, :monthly_payment, :description, :plan_category_id, + :machines_visibility, plan_file_attributes: %i[id attachment _destroy], prices_attributes: %i[id amount], - advanced_accounting_attributes: %i[code analytical_section]) + advanced_accounting_attributes: %i[code analytical_section], + plan_limitations_attributes: %i[id limitable_id limitable_type limit _destroy]) end end end diff --git a/app/controllers/api/prices_controller.rb b/app/controllers/api/prices_controller.rb index ab48e119e..5c2d219e1 100644 --- a/app/controllers/api/prices_controller.rb +++ b/app/controllers/api/prices_controller.rb @@ -46,6 +46,9 @@ class API::PricesController < API::ApiController cs = CartService.new(current_user) cart = cs.from_hash(params) @amount = cart.total + # TODO, remove this when the cart is refactored + cart.valid? + @errors = cart.errors end private diff --git a/app/controllers/api/stripe_controller.rb b/app/controllers/api/stripe_controller.rb index 05b3a6e2b..e885c6fd4 100644 --- a/app/controllers/api/stripe_controller.rb +++ b/app/controllers/api/stripe_controller.rb @@ -19,7 +19,7 @@ class API::StripeController < API::PaymentsController res = nil # json of the API answer cart = shopping_cart - render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid? + render json: cart.errors, status: :unprocessable_entity and return unless cart.valid? begin amount = debit_amount(cart) # will contains the amount and the details of each invoice lines @@ -73,7 +73,7 @@ class API::StripeController < API::PaymentsController def setup_subscription cart = shopping_cart - render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid? + render json: cart.errors, status: :unprocessable_entity and return unless cart.valid? service = Stripe::Service.new method = service.attach_method_as_default( diff --git a/app/controllers/api/wallet_controller.rb b/app/controllers/api/wallet_controller.rb index dd03fb1b8..19eedffea 100644 --- a/app/controllers/api/wallet_controller.rb +++ b/app/controllers/api/wallet_controller.rb @@ -25,7 +25,7 @@ class API::WalletController < API::ApiController service = WalletService.new(user: current_user, wallet: @wallet) transaction = service.credit(credit_params[:amount].to_f) if transaction - service.create_avoir(transaction, credit_params[:avoir_date], credit_params[:avoir_description]) if credit_params[:avoir] + service.create_avoir(transaction, credit_params[:avoir_description]) if credit_params[:avoir] render :show else head :unprocessable_entity @@ -35,6 +35,6 @@ class API::WalletController < API::ApiController private def credit_params - params.permit(:id, :amount, :avoir, :avoir_date, :avoir_description) + params.permit(:id, :amount, :avoir, :avoir_description) end end diff --git a/app/frontend/src/javascript/components/base/edit-destroy-buttons.tsx b/app/frontend/src/javascript/components/base/edit-destroy-buttons.tsx index 16ca23496..3add0e256 100644 --- a/app/frontend/src/javascript/components/base/edit-destroy-buttons.tsx +++ b/app/frontend/src/javascript/components/base/edit-destroy-buttons.tsx @@ -5,24 +5,34 @@ import { useTranslation } from 'react-i18next'; import { FabButton } from './fab-button'; import { FabModal } from './fab-modal'; -interface EditDestroyButtonsProps { - onDeleteSuccess: (message: string) => void, +type EditDestroyButtonsCommon = { onError: (message: string) => void, onEdit: () => void, itemId: number, - itemType: string, - apiDestroy: (itemId: number) => Promise, - confirmationMessage?: string|ReactNode, + destroy: (itemId: number) => Promise, className?: string, - iconSize?: number + iconSize?: number, + showEditButton?: boolean, } +type DeleteSuccess = + { onDeleteSuccess: (message: string) => void, deleteSuccessMessage: string } | + { onDeleteSuccess?: never, deleteSuccessMessage?: never } + +type DestroyMessages = + ({ showDestroyConfirmation?: true } & + ({ itemType: string, confirmationTitle?: string, confirmationMessage?: string|ReactNode } | + { itemType?: never, confirmationTitle: string, confirmationMessage: string|ReactNode })) | + { showDestroyConfirmation: false, itemType?: never, confirmationTitle?: never, confirmationMessage?: never }; + +type EditDestroyButtonsProps = EditDestroyButtonsCommon & DeleteSuccess & DestroyMessages; + /** * This component shows a group of two buttons. * Destroy : shows a modal dialog to ask the user for confirmation about the deletion of the provided item. * Edit : triggers the provided function. */ -export const EditDestroyButtons: React.FC = ({ onDeleteSuccess, onError, onEdit, itemId, itemType, apiDestroy, confirmationMessage, className, iconSize = 20 }) => { +export const EditDestroyButtons: React.FC = ({ onDeleteSuccess, onError, onEdit, itemId, itemType, destroy, confirmationTitle, confirmationMessage, deleteSuccessMessage, className, iconSize = 20, showEditButton = true, showDestroyConfirmation = true }) => { const { t } = useTranslation('admin'); const [deletionModal, setDeletionModal] = useState(false); @@ -34,30 +44,41 @@ export const EditDestroyButtons: React.FC = ({ onDelete setDeletionModal(!deletionModal); }; + /** + * Triggered when the user clicks on the 'destroy' button + */ + const handleDestroyRequest = (): void => { + if (showDestroyConfirmation) { + toggleDeletionModal(); + } else { + onDeleteConfirmed(); + } + }; + /** * The deletion has been confirmed by the user. * Call the API to trigger the deletion of the given item */ const onDeleteConfirmed = (): void => { - apiDestroy(itemId).then(() => { - onDeleteSuccess(t('app.admin.edit_destroy_buttons.deleted', { TYPE: itemType })); + destroy(itemId).then(() => { + typeof onDeleteSuccess === 'function' && onDeleteSuccess(deleteSuccessMessage || t('app.admin.edit_destroy_buttons.deleted')); }).catch((error) => { - onError(t('app.admin.edit_destroy_buttons.unable_to_delete', { TYPE: itemType }) + error); + onError(t('app.admin.edit_destroy_buttons.unable_to_delete') + error); }); - toggleDeletionModal(); + setDeletionModal(false); }; return ( <>
- + {showEditButton && - - + } +
- ({ id, re return num; } if (type === 'date') { - const date: Date = new Date(value); + const date: Date = new Date(value + 'T00:00:00'); if (Number.isNaN(date) && nullable) { return null; } diff --git a/app/frontend/src/javascript/components/form/form-select.tsx b/app/frontend/src/javascript/components/form/form-select.tsx index ed62fb163..76512dd29 100644 --- a/app/frontend/src/javascript/components/form/form-select.tsx +++ b/app/frontend/src/javascript/components/form/form-select.tsx @@ -58,10 +58,10 @@ export const FormSelect = c.value === value)} + value={value === null ? null : options.find(c => c.value === value)} onChange={val => { - onChangeCb(val.value); - onChange(val.value); + onChangeCb(val?.value); + onChange(val?.value); }} placeholder={placeholder} isDisabled={isDisabled} diff --git a/app/frontend/src/javascript/components/form/form-unsaved-list.tsx b/app/frontend/src/javascript/components/form/form-unsaved-list.tsx new file mode 100644 index 000000000..7a1099d24 --- /dev/null +++ b/app/frontend/src/javascript/components/form/form-unsaved-list.tsx @@ -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, TKeyName extends string> { + fields: Array>, + onRemove: (index: number) => void, + register: UseFormRegister, + className?: string, + title: string, + shouldRenderField?: (field: FieldArrayWithId) => boolean, + renderField: (field: FieldArrayWithId) => ReactNode, + formAttributeName: `${string}_attributes`, + formAttributes: Array>, + 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: + * ``` + * <> + *
+ * Attribute 1 + *

{item.attr1}

+ *
+ *
+ * ... + *
+ * + * ``` + */ +export const FormUnsavedList = = FieldArrayPath, TKeyName extends string = 'id'>({ fields, onRemove, register, className, title, shouldRenderField = () => true, renderField, formAttributeName, formAttributes, saveReminderLabel, cancelLabel }: FormUnsavedListProps) => { + const { t } = useTranslation('shared'); + + /** + * Render an unsaved field + */ + const renderUnsavedField = (field: FieldArrayWithId, index: number): ReactNode => { + return ( +
+ {renderField(field)} +

onRemove(index)}> + {cancelLabel || t('app.shared.form_unsaved_list.cancel')} + +

+ {formAttributes.map((attribute, attrIndex) => ( + + ))} +
+ ); + }; + + if (fields.filter(shouldRenderField).length === 0) return null; + + return ( +
+ {title} + {saveReminderLabel || t('app.shared.form_unsaved_list.save_reminder')} + {fields.map((field, index) => { + if (!shouldRenderField(field)) return false; + return renderUnsavedField(field, index); + }).filter(Boolean)} +
+ ); +}; diff --git a/app/frontend/src/javascript/components/machines/machine-categories-list.tsx b/app/frontend/src/javascript/components/machines/machine-categories-list.tsx index addc6ae43..430783fbb 100644 --- a/app/frontend/src/javascript/components/machines/machine-categories-list.tsx +++ b/app/frontend/src/javascript/components/machines/machine-categories-list.tsx @@ -124,7 +124,7 @@ export const MachineCategoriesList: React.FC = ({ on onEdit={editMachineCategory(category)} itemId={category.id} itemType={t('app.admin.machine_categories_list.machine_category')} - apiDestroy={MachineCategoryAPI.destroy} /> + destroy={MachineCategoryAPI.destroy} /> diff --git a/app/frontend/src/javascript/components/plans/plan-form.tsx b/app/frontend/src/javascript/components/plans/plan-form.tsx index 44c4cbe6d..2c95ecd5f 100644 --- a/app/frontend/src/javascript/components/plans/plan-form.tsx +++ b/app/frontend/src/javascript/components/plans/plan-form.tsx @@ -24,6 +24,10 @@ import { UserPlus } from 'phosphor-react'; import { PartnerModal } from './partner-modal'; import { PlanPricingForm } from './plan-pricing-form'; import { AdvancedAccountingForm } from '../accounting/advanced-accounting-form'; +import { FabTabs } from '../base/fab-tabs'; +import { PlanLimitForm } from './plan-limit-form'; +import { UnsavedFormAlert } from '../form/unsaved-form-alert'; +import { UIRouter } from '@uirouter/angularjs'; declare const Application: IApplication; @@ -33,13 +37,14 @@ interface PlanFormProps { onError: (message: string) => void, onSuccess: (message: string) => void, beforeSubmit?: (data: Plan) => void, + uiRouter: UIRouter } /** * Form to edit or create subscription plans */ -export const PlanForm: React.FC = ({ action, plan, onError, onSuccess, beforeSubmit }) => { - const { handleSubmit, register, control, formState, setValue } = useForm({ defaultValues: { ...plan } }); +export const PlanForm: React.FC = ({ action, plan, onError, onSuccess, beforeSubmit, uiRouter }) => { + const { handleSubmit, register, control, formState, setValue, getValues, resetField } = useForm({ defaultValues: { ...plan } }); const output = useWatch({ control }); // eslint-disable-line const { t } = useTranslation('admin'); @@ -51,13 +56,19 @@ export const PlanForm: React.FC = ({ action, plan, onError, onSuc useEffect(() => { GroupAPI.index({ disabled: false }) - .then(res => setGroups(res.map(g => { return { value: g.id, label: g.name }; }))) + .then(res => setGroups(res.map(g => { + return { value: g.id, label: g.name }; + }))) .catch(onError); PlanCategoryAPI.index() - .then(res => setCategories(res.map(c => { return { value: c.id, label: c.name }; }))) + .then(res => setCategories(res.map(c => { + return { value: c.id, label: c.name }; + }))) .catch(onError); UserAPI.index({ role: 'partner' }) - .then(res => setPartners(res.map(p => { return { value: p.id, label: p.name }; }))) + .then(res => setPartners(res.map(p => { + return { value: p.id, label: p.name }; + }))) .catch(onError); }, []); @@ -101,7 +112,9 @@ export const PlanForm: React.FC = ({ action, plan, onError, onSuc * Return the available options for the plan period */ const buildPeriodsOptions = (): Array> => { - 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,226 @@ export const PlanForm: React.FC = ({ action, plan, onError, onSuc setValue('partner_id', user.id); }; - return ( -
-
-

{t('app.admin.plan_form.general_information')}

- - {action === 'create' && } - {!allGroups && groups && } - {categories?.length > 0 && } - {action === 'update' && - {t('app.admin.plan_form.edit_amount_info')} - } - - - - - - - -

{t('app.admin.plan_form.duration')}

-
+ /** + * Render the content of the 'subscriptions settings' tab + */ + const renderSettingsTab = () => ( +
+
+
+

{t('app.admin.plan_form.description')}

+
+
- + rules={{ + required: true, + maxLength: { value: 24, message: t('app.admin.plan_form.name_max_length') } + }} + label={t('app.admin.plan_form.name')} /> + +
-

{t('app.admin.plan_form.partnership')}

-
+
+ +
+
+

{t('app.admin.plan_form.general_settings')}

+

{t('app.admin.plan_form.general_settings_info')}

+
+
+ {action === 'create' && } + {!allGroups && groups && } +
+ + +
+ {action === 'update' && + {t('app.admin.plan_form.edit_amount_info')} + } + +
+
+ +
+
+

{t('app.admin.plan_form.activation_and_payment')}

+
+
+ + + +
+
+ +
+
+

{t('app.admin.plan_form.partnership')}

+

{t('app.admin.plan_form.partner_plan_help')}

+
+
{output.type === 'PartnerPlan' &&
- } onClick={tooglePartnerModal}> - {t('app.admin.plan_form.new_user')} - {partners && } - {output.partner_id && - {t('app.admin.plan_form.alert_partner_notification')} - } + } onClick={tooglePartnerModal}> + {t('app.admin.plan_form.new_user')} +
}
- - {action === 'update' && + +
+
+

{t('app.admin.plan_form.slots_visibility')}

+

{t('app.admin.plan_form.slots_visibility_help')}

+
+
+ { return (v === null || v >= 7 || t('app.admin.plan_form.visibility_minimum') as string); } }} + type="number" + label={t('app.admin.plan_form.machines_visibility')} /> +
+
+ +
+
+

{t('app.admin.plan_form.display')}

+
+
+ {categories?.length > 0 && } - - {t('app.admin.plan_form.ACTION_plan', { ACTION: action })} - + id="plan_category_id" + tooltip={t('app.admin.plan_form.category_help')} + label={t('app.admin.plan_form.category')} />} + +
+
+ +
+ +
+ + {action === 'update' && } +
+ ); + + return ( +
+
+

{t('app.admin.plan_form.ACTION_title', { ACTION: action })}

+
+ + {t('app.admin.plan_form.save')} + +
+
+ + + + + } + ]} /> + = (props) => { ); }; -Application.Components.component('planForm', react2angular(PlanFormWrapper, ['action', 'plan', 'onError', 'onSuccess'])); +Application.Components.component('planForm', react2angular(PlanFormWrapper, ['action', 'plan', 'onError', 'onSuccess', 'uiRouter'])); diff --git a/app/frontend/src/javascript/components/plans/plan-limit-form.tsx b/app/frontend/src/javascript/components/plans/plan-limit-form.tsx new file mode 100644 index 000000000..601dcce53 --- /dev/null +++ b/app/frontend/src/javascript/components/plans/plan-limit-form.tsx @@ -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 { + register: UseFormRegister, + control: Control, + formState: FormState, + onError: (message: string) => void, + getValues: UseFormGetValues, + resetField: UseFormResetField +} + +/** + * Form tab to manage a subscription's usage limit + */ +export const PlanLimitForm = ({ register, control, formState, onError, getValues, resetField }: PlanLimitFormProps) => { + const { t } = useTranslation('admin'); + const { fields, append, remove, update } = useFieldArray({ control, name: 'plan_limitations_attributes' }); + const limiting = useWatch({ control, name: 'limiting' }); + + const [isOpen, setIsOpen] = useState(false); + const [machines, setMachines] = useState>([]); + const [categories, setCategories] = useState>([]); + 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 => { + return () => { + return new Promise((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) => 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' &&
+ {t('app.admin.plan_limit_form.category')} +

{categories?.find(c => c.id === limit.limitable_id)?.name}

+
) || +
+ {t('app.admin.plan_limit_form.machine')} +

{machines?.find(m => m.id === limit.limitable_id)?.name}

+
} +
+ {t('app.admin.plan_limit_form.max_hours_per_day')} +

{limit.limit}

+
+ + ); + + return ( +
+
+
+

{t('app.admin.plan_limit_form.usage_limitation')}

+

{t('app.admin.plan_limit_form.usage_limitation_info')}

+
+
+ +
+
+ + {limiting &&
+
+

{t('app.admin.plan_limit_form.all_limitations')}

+
+ + {t('app.admin.plan_limit_form.new_usage_limitation')} + +
+
+ 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 && +

{t('app.admin.plan_limit_form.saved_limitations')}

+ } + + {fields.filter(f => f.limitable_type === 'MachineCategory' && !f._modified).length > 0 && +
+

{t('app.admin.plan_limit_form.by_category')}

+ {fields.map((limitation, index) => { + if (limitation.limitable_type !== 'MachineCategory' || limitation._modified) return false; + + return ( +
+
+
+ {t('app.admin.plan_limit_form.category')} +

{categories.find(c => c.id === limitation.limitable_id)?.name}

+
+
+ {t('app.admin.plan_limit_form.max_hours_per_day')} +

{limitation.limit}

+
+
+ +
+ +
+
+ ); + }).filter(Boolean)} +
+ } + + {fields.filter(f => f.limitable_type === 'Machine' && !f._modified).length > 0 && +
+

{t('app.admin.plan_limit_form.by_machine')}

+ {fields.map((limitation, index) => { + if (limitation.limitable_type !== 'Machine' || limitation._modified) return false; + + return ( +
+
+
+ {t('app.admin.plan_limit_form.machine')} +

{machines.find(m => m.id === limitation.limitable_id)?.name}

+
+
+ {t('app.admin.plan_limit_form.max_hours_per_day')} +

{limitation.limit}

+
+ {limitation._destroy &&
{t('app.admin.plan_limit_form.ongoing_deletion')}
} +
+ +
+ {(limitation._destroy && +

+ {t('app.admin.plan_limit_form.cancel_deletion')} + +

) || + } +
+
+ ); + }).filter(Boolean)} +
+ } +
} + + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/plans/plan-limit-modal.tsx b/app/frontend/src/javascript/components/plans/plan-limit-modal.tsx new file mode 100644 index 000000000..2ae2dc62a --- /dev/null +++ b/app/frontend/src/javascript/components/plans/plan-limit-modal.tsx @@ -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 + categories: Array, + limitation?: PlanLimitation, + existingLimitations: Array; +} + +/** + * Form to manage subscriptions limitations of use + */ +export const PlanLimitModal: React.FC = ({ isOpen, toggleModal, machines, categories, onSuccess, limitation, existingLimitations = [] }) => { + const { t } = useTranslation('admin'); + + const { register, control, formState, setValue, handleSubmit, reset } = useForm({ 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, 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) => { + 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> => { + 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 ( + reset({ limitable_type: 'MachineCategory' })} + closeButton> +
+

{t('app.admin.plan_limit_modal.limit_reservations')}

+
+ + +
+ {limitType === 'Machine' ? t('app.admin.plan_limit_modal.machine_info') : t('app.admin.plan_limit_modal.categories_info')} + + + + + {t('app.admin.plan_limit_modal.confirm')} + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/plans/plan-pricing-form.tsx b/app/frontend/src/javascript/components/plans/plan-pricing-form.tsx index 637ecfe76..247b9b60b 100644 --- a/app/frontend/src/javascript/components/plans/plan-pricing-form.tsx +++ b/app/frontend/src/javascript/components/plans/plan-pricing-form.tsx @@ -92,32 +92,37 @@ export const PlanPricingForm = ({ register, control, fo }; return ( -
-

{t('app.admin.plan_pricing_form.prices')}

- {plans && } - { { - if (price.priceable_type !== 'Machine') return false; - return renderPriceElement(price, index); - }).filter(Boolean) - }, - spaces && { - id: 'spaces', - title: t('app.admin.plan_pricing_form.spaces'), - content: fields.map((price, index) => { - if (price.priceable_type !== 'Space') return false; - return renderPriceElement(price, index); - }).filter(Boolean) - } - ]} />} -
+
+
+

{t('app.admin.plan_pricing_form.prices')}

+

{t('app.admin.plan_pricing_form.about_prices')}

+
+
+ {plans && } + { { + if (price.priceable_type !== 'Machine') return false; + return renderPriceElement(price, index); + }).filter(Boolean) + }, + spaces && { + id: 'spaces', + title: t('app.admin.plan_pricing_form.spaces'), + content: fields.map((price, index) => { + if (price.priceable_type !== 'Space') return false; + return renderPriceElement(price, index); + }).filter(Boolean) + } + ]} />} +
+
); }; diff --git a/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx b/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx index 3a2aa103f..de1126bad 100644 --- a/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx @@ -114,7 +114,7 @@ export const ConfigurePacksButton: React.FC = ({ pack onEdit={() => handleRequestEdit(p)} itemId={p.id} itemType={t('app.admin.configure_packs_button.pack')} - apiDestroy={PrepaidPackAPI.destroy}/> + destroy={PrepaidPackAPI.destroy}/> void, - onDelete: (productId: number) => void, + onDelete: (message: string) => void, + onError: (message: string) => void, } /** * This component shows a product item in the admin view */ -export const ProductItem: React.FC = ({ product, onEdit, onDelete }) => { +export const ProductItem: React.FC = ({ product, onEdit, onDelete, onError }) => { const { t } = useTranslation('admin'); /** @@ -34,15 +35,6 @@ export const ProductItem: React.FC = ({ product, onEdit, onDel }; }; - /** - * Init the process of delete the given product - */ - const deleteProduct = (productId: number): () => void => { - return (): void => { - onDelete(productId); - }; - }; - /** * Returns CSS class from stock status */ @@ -80,14 +72,13 @@ export const ProductItem: React.FC = ({ product, onEdit, onDel
-
- - - - - - -
+
); diff --git a/app/frontend/src/javascript/components/store/product-stock-form.tsx b/app/frontend/src/javascript/components/store/product-stock-form.tsx index bf12a121f..b2cb4cc0a 100644 --- a/app/frontend/src/javascript/components/store/product-stock-form.tsx +++ b/app/frontend/src/javascript/components/store/product-stock-form.tsx @@ -1,11 +1,11 @@ -import { useEffect, useState } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import Select from 'react-select'; -import { PencilSimple, X } from 'phosphor-react'; +import { PencilSimple } from 'phosphor-react'; import { useFieldArray, UseFormRegister } from 'react-hook-form'; import { Control, FormState, UseFormSetValue } from 'react-hook-form/dist/types/form'; import { useTranslation } from 'react-i18next'; import { - Product, + Product, ProductStockMovement, stockMovementAllReasons, StockMovementIndex, StockMovementIndexFilter, StockMovementReason, StockType @@ -20,6 +20,7 @@ import FormatLib from '../../lib/format'; import ProductLib from '../../lib/product'; import { useImmer } from 'use-immer'; import { FabPagination } from '../base/fab-pagination'; +import { FormUnsavedList } from '../form/form-unsaved-list'; interface ProductStockFormProps { currentFormValues: Product, @@ -159,6 +160,25 @@ export const ProductStockForm = ({ currentFormValues, } }; + /** + * Render an attribute of an unsaved stock movement + */ + const renderOngoingStockMovement = (movement: ProductStockMovement): ReactNode => ( + <> +
+

{t(`app.admin.store.product_stock_form.type_${ProductLib.stockMovementType(movement.reason)}`)}

+
+
+ {t(`app.admin.store.product_stock_form.${movement.stock_type}`)} +

{ProductLib.absoluteStockMovement(movement.quantity, movement.reason)}

+
+
+ {t('app.admin.store.product_stock_form.reason')} +

{t(ProductLib.stockMovementReasonTrKey(movement.reason))}

+
+ + ); + return (

{t('app.admin.store.product_stock_form.stock_up_to_date')}  @@ -178,36 +198,19 @@ export const ProductStockForm = ({ currentFormValues, {t('app.admin.store.product_stock_form.external')}

{currentFormValues?.stock?.external}

- } className="is-black">Modifier + } className="is-black">{t('app.admin.store.product_stock_form.edit')}
- {fields.length > 0 &&
- {t('app.admin.store.product_stock_form.ongoing_operations')} - {t('app.admin.store.product_stock_form.save_reminder')} - {fields.map((newMovement, index) => ( -
-
-

{t(`app.admin.store.product_stock_form.type_${ProductLib.stockMovementType(newMovement.reason)}`)}

-
-
- {t(`app.admin.store.product_stock_form.${newMovement.stock_type}`)} -

{ProductLib.absoluteStockMovement(newMovement.quantity, newMovement.reason)}

-
-
- {t('app.admin.store.product_stock_form.reason')} -

{t(ProductLib.stockMovementReasonTrKey(newMovement.reason))}

-
-

remove(index)}> - {t('app.admin.store.product_stock_form.cancel')} - -

- - - -
- ))} -
} +
diff --git a/app/frontend/src/javascript/components/store/products.tsx b/app/frontend/src/javascript/components/store/products.tsx index 2f94b9367..1f55aa2c5 100644 --- a/app/frontend/src/javascript/components/store/products.tsx +++ b/app/frontend/src/javascript/components/store/products.tsx @@ -111,14 +111,9 @@ const Products: React.FC = ({ onSuccess, onError, uiRouter }) => }; /** Delete a product */ - const deleteProduct = async (productId: number): Promise => { - try { - await ProductAPI.destroy(productId); - await fetchProducts(); - onSuccess(t('app.admin.store.products.successfully_deleted')); - } catch (e) { - onError(t('app.admin.store.products.unable_to_delete') + e); - } + const deleteProduct = async (message: string): Promise => { + await fetchProducts(); + onSuccess(message); }; /** Goto new product page */ @@ -244,6 +239,7 @@ const Products: React.FC = ({ onSuccess, onError, uiRouter }) => diff --git a/app/frontend/src/javascript/components/trainings/trainings.tsx b/app/frontend/src/javascript/components/trainings/trainings.tsx index 3ff49b787..f510507d2 100644 --- a/app/frontend/src/javascript/components/trainings/trainings.tsx +++ b/app/frontend/src/javascript/components/trainings/trainings.tsx @@ -199,7 +199,7 @@ export const Trainings: React.FC = ({ onError, onSuccess }) => { onEdit={() => toTrainingEdit(training)} itemId={training.id} itemType={t('app.admin.trainings.training')} - apiDestroy={TrainingAPI.destroy}/> + destroy={TrainingAPI.destroy}/> ))} diff --git a/app/frontend/src/javascript/components/user/gender-input.tsx b/app/frontend/src/javascript/components/user/gender-input.tsx index 66d3dc071..5958fe753 100644 --- a/app/frontend/src/javascript/components/user/gender-input.tsx +++ b/app/frontend/src/javascript/components/user/gender-input.tsx @@ -7,12 +7,13 @@ import { useTranslation } from 'react-i18next'; interface GenderInputProps { register: UseFormRegister, disabled?: boolean|((id: string) => boolean), + required?: boolean } /** * Input component to set the gender for the user */ -export const GenderInput = ({ register, disabled = false }: GenderInputProps) => { +export const GenderInput = ({ register, disabled = false, required }: GenderInputProps) => { const { t } = useTranslation('shared'); const [isDisabled, setIsDisabled] = useState(false); @@ -26,21 +27,25 @@ export const GenderInput = ({ register, disabl }, [disabled]); return ( -
+
+ {t('app.shared.gender_input.label')} -
+ ); }; diff --git a/app/frontend/src/javascript/components/user/user-profile-form.tsx b/app/frontend/src/javascript/components/user/user-profile-form.tsx index f5125fb23..940315ed7 100644 --- a/app/frontend/src/javascript/components/user/user-profile-form.tsx +++ b/app/frontend/src/javascript/components/user/user-profile-form.tsx @@ -184,7 +184,7 @@ export const UserProfileForm: React.FC = ({ action, size,

{t('app.shared.user_profile_form.personal_data')}

- +
0) { + for (const error in res.errors) { + for (const message of res.errors[error]) { + growl.error(message); + } + } + } setSlotsDetails(res.details); }); } else { diff --git a/app/frontend/src/javascript/models/plan.ts b/app/frontend/src/javascript/models/plan.ts index 6b7656521..f8d1b93c3 100644 --- a/app/frontend/src/javascript/models/plan.ts +++ b/app/frontend/src/javascript/models/plan.ts @@ -12,6 +12,16 @@ export interface Partner { email: string } +export type LimitableType = 'Machine'|'MachineCategory'; +export interface PlanLimitation { + id?: number, + limitable_id: number, + limitable_type: LimitableType, + limit: number, + _modified?: boolean, + _destroy?: boolean, +} + export interface Plan { id?: number, base_name: string, @@ -34,8 +44,10 @@ export interface Plan { plan_file_url?: string, partner_id?: number, partnership?: boolean, + limiting?: boolean, partners?: Array, - advanced_accounting_attributes?: AdvancedAccounting + advanced_accounting_attributes?: AdvancedAccounting, + plan_limitations_attributes?: Array } export interface PlansDuration { diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index 6337466d2..27f84db14 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -81,7 +81,10 @@ export const bookingSettings = [ 'reminder_delay', 'visibility_yearly', 'visibility_others', - 'reservation_deadline', + 'machine_reservation_deadline', + 'training_reservation_deadline', + 'event_reservation_deadline', + 'space_reservation_deadline', 'display_name_enable', 'book_overlapping_slots', 'slot_duration', diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index d54da8537..f2186e456 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -1175,7 +1175,8 @@ angular.module('application.router', ['ui.router']) "'renew_pack_threshold', 'pack_only_for_subscription', 'overlapping_categories', 'public_registrations'," + "'extended_prices_in_same_day', 'recaptcha_site_key', 'recaptcha_secret_key', 'user_validation_required', " + "'user_validation_required_list', 'machines_module', 'user_change_group', 'show_username_in_admin_list', " + - "'store_module', 'reservation_deadline']" + "'store_module', 'machine_reservation_deadline', 'training_reservation_deadline', 'event_reservation_deadline', " + + "'space_reservation_deadline']" }).$promise; }], privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }], diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index 22fc9555c..b70e769c6 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -62,6 +62,7 @@ @import "modules/form/form-checklist"; @import "modules/form/form-file-upload"; @import "modules/form/form-image-upload"; +@import "modules/form/form-unsaved-list"; @import "modules/group/change-group"; @import "modules/invoices/invoices-settings-panel"; @import "modules/invoices/vat-settings-modal"; @@ -97,6 +98,9 @@ @import "modules/plan-categories/plan-categories-list"; @import "modules/plans/plan-card"; @import "modules/plans/plan-form"; +@import "modules/plans/plan-limit-form"; +@import "modules/plans/plan-limit-modal"; +@import "modules/plans/plan-pricing-form"; @import "modules/plans/plans-filter"; @import "modules/plans/plans-list"; @import "modules/prepaid-packs/packs-summary"; diff --git a/app/frontend/src/stylesheets/modules/base/fab-tabs.scss b/app/frontend/src/stylesheets/modules/base/fab-tabs.scss index c53fabd24..ecffcfd65 100644 --- a/app/frontend/src/stylesheets/modules/base/fab-tabs.scss +++ b/app/frontend/src/stylesheets/modules/base/fab-tabs.scss @@ -12,16 +12,21 @@ text-align: center; color: var(--main); border-bottom: 1px solid var(--gray-soft-dark); + border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0; + &:hover { + background-color: var(--gray-soft-light); + cursor: pointer; + } &.react-tabs__tab--selected { color: var(--gray-hard-dark); border: 1px solid var(--gray-soft-dark); border-bottom: none; - border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0; - } - - &:hover { - cursor: pointer; + &:hover { + background-color: var(--gray-soft-lightest); + cursor: default; + } + &:focus { outline: none; } } } } diff --git a/app/frontend/src/stylesheets/modules/base/fab-text-editor.scss b/app/frontend/src/stylesheets/modules/base/fab-text-editor.scss index 430c0c383..5350c1ed6 100644 --- a/app/frontend/src/stylesheets/modules/base/fab-text-editor.scss +++ b/app/frontend/src/stylesheets/modules/base/fab-text-editor.scss @@ -1,6 +1,6 @@ .fab-text-editor { position: relative; - margin-bottom: 1.6rem; + //margin-bottom: 1.6rem; padding-bottom: 1.6rem; background-color: var(--gray-soft-lightest); border: 1px solid var(--gray-soft-dark); diff --git a/app/frontend/src/stylesheets/modules/events/event-form.scss b/app/frontend/src/stylesheets/modules/events/event-form.scss index 76fb5ec71..92d91a1ae 100644 --- a/app/frontend/src/stylesheets/modules/events/event-form.scss +++ b/app/frontend/src/stylesheets/modules/events/event-form.scss @@ -15,8 +15,6 @@ flex-direction: column; gap: 3.2rem; - .fab-alert { margin: 0; } - section { @include layout-settings; } .save-btn { align-self: flex-start; } } @@ -25,7 +23,7 @@ display: flex; flex-direction: column; align-items: flex-end; - gap: 2.4rem; + gap: 1.6rem 2.4rem; .add-price { max-width: fit-content; @@ -37,6 +35,8 @@ align-items: flex-end; gap: 0 2.4rem; + .form-item { margin: 0; } + .remove-price { align-items: center; display: flex; @@ -61,8 +61,7 @@ flex-direction: column; @media (min-width: 640px) {flex-direction: row; } - .form-item:first-child { - margin-right: 2.4rem; - } + .form-item { margin: 0; } + .form-item:first-child { margin-right: 2.4rem; } } } diff --git a/app/frontend/src/stylesheets/modules/form/form-unsaved-list.scss b/app/frontend/src/stylesheets/modules/form/form-unsaved-list.scss new file mode 100644 index 000000000..f7b1a37dc --- /dev/null +++ b/app/frontend/src/stylesheets/modules/form/form-unsaved-list.scss @@ -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; + } + } + } +} diff --git a/app/frontend/src/stylesheets/modules/machines/machine-form.scss b/app/frontend/src/stylesheets/modules/machines/machine-form.scss index 90c099238..28364b434 100644 --- a/app/frontend/src/stylesheets/modules/machines/machine-form.scss +++ b/app/frontend/src/stylesheets/modules/machines/machine-form.scss @@ -15,8 +15,6 @@ flex-direction: column; gap: 3.2rem; - .fab-alert { margin: 0; } - section { @include layout-settings; } .save-btn { align-self: flex-start; } } diff --git a/app/frontend/src/stylesheets/modules/plans/plan-form.scss b/app/frontend/src/stylesheets/modules/plans/plan-form.scss index 8366b0aee..7dde64762 100644 --- a/app/frontend/src/stylesheets/modules/plans/plan-form.scss +++ b/app/frontend/src/stylesheets/modules/plans/plan-form.scss @@ -1,25 +1,40 @@ .plan-form { - .plan-sheet { - margin-top: 4rem; + max-width: 1260px; + margin: 2.4rem auto 0; + padding: 0 3rem 6rem; + display: flex; + flex-direction: column; + & > header { + padding-bottom: 0; + @include header($sticky: true); + gap: 2.4rem; } - .duration { - display: flex; - flex-direction: row; - .form-item:first-child { - margin-right: 32px; - } - } - .partner { + &-content { display: flex; flex-direction: column; - align-items: flex-end; + gap: 3.2rem; - .fab-alert { - width: 100%; + section { @include layout-settings; } + .grp { + display: flex; + flex-direction: column; + @media (min-width: 640px) {flex-direction: row; } + + .form-item { margin: 0; } + .form-item:first-child { margin-right: 2.4rem; } + } + + .partner { + display: flex; + flex-direction: column-reverse; + align-items: flex-end; + gap: 0 2.4rem; + + @media (min-width: 640px) { + flex-direction: row; + button { margin-bottom: 1.6rem; } + } } } - .submit-btn { - float: right; - } } diff --git a/app/frontend/src/stylesheets/modules/plans/plan-limit-form.scss b/app/frontend/src/stylesheets/modules/plans/plan-limit-form.scss new file mode 100644 index 000000000..ef264a663 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/plans/plan-limit-form.scss @@ -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; + } + } + } + } + } + } +} diff --git a/app/frontend/src/stylesheets/modules/plans/plan-limit-modal.scss b/app/frontend/src/stylesheets/modules/plans/plan-limit-modal.scss new file mode 100644 index 000000000..08d11d0eb --- /dev/null +++ b/app/frontend/src/stylesheets/modules/plans/plan-limit-modal.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/app/frontend/src/stylesheets/modules/plans/plan-pricing-form.scss b/app/frontend/src/stylesheets/modules/plans/plan-pricing-form.scss new file mode 100644 index 000000000..68a9e5756 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/plans/plan-pricing-form.scss @@ -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; + } +} \ No newline at end of file diff --git a/app/frontend/src/stylesheets/modules/spaces/space-form.scss b/app/frontend/src/stylesheets/modules/spaces/space-form.scss index 0db00f748..f8caba094 100644 --- a/app/frontend/src/stylesheets/modules/spaces/space-form.scss +++ b/app/frontend/src/stylesheets/modules/spaces/space-form.scss @@ -15,8 +15,6 @@ flex-direction: column; gap: 3.2rem; - .fab-alert { margin: 0; } - section { @include layout-settings; } .save-btn { align-self: flex-start; } } diff --git a/app/frontend/src/stylesheets/modules/store/product-stock-form.scss b/app/frontend/src/stylesheets/modules/store/product-stock-form.scss index 5290c7a80..1e9a2bd78 100644 --- a/app/frontend/src/stylesheets/modules/store/product-stock-form.scss +++ b/app/frontend/src/stylesheets/modules/store/product-stock-form.scss @@ -3,28 +3,6 @@ .ongoing-stocks { margin: 2.4rem 0; - .save-notice { - @include text-xs; - margin-left: 1rem; - color: var(--alert); - } - .unsaved-stock-movement { - background-color: var(--gray-soft-light); - border: 0; - padding: 1.2rem; - margin-top: 1rem; - - .cancel-action { - &:hover { - text-decoration: underline; - cursor: pointer; - } - svg { - margin-left: 1rem; - vertical-align: middle; - } - } - } } .store-list { margin-top: 2.4rem; diff --git a/app/frontend/src/stylesheets/modules/trainings/training-form.scss b/app/frontend/src/stylesheets/modules/trainings/training-form.scss index adf701132..1eeb90524 100644 --- a/app/frontend/src/stylesheets/modules/trainings/training-form.scss +++ b/app/frontend/src/stylesheets/modules/trainings/training-form.scss @@ -15,8 +15,6 @@ flex-direction: column; gap: 3.2rem; - .fab-alert { margin: 0; } - section { @include layout-settings; } .save-btn { align-self: flex-start; } } diff --git a/app/frontend/src/stylesheets/modules/user/gender-input.scss b/app/frontend/src/stylesheets/modules/user/gender-input.scss index f639c5eee..f72452b76 100644 --- a/app/frontend/src/stylesheets/modules/user/gender-input.scss +++ b/app/frontend/src/stylesheets/modules/user/gender-input.scss @@ -1,5 +1,17 @@ .gender-input { margin-bottom: 1.6rem; + legend { + @include text-sm; + margin: 0 0 0.8rem; + border: none; + cursor: pointer; + &::first-letter { text-transform: uppercase; } + &.is-required::after { + content: "*"; + margin-left: 0.5ch; + color: var(--alert); + } + } label { display: inline-flex; diff --git a/app/frontend/src/stylesheets/variables/layout.scss b/app/frontend/src/stylesheets/variables/layout.scss index 1717818c5..b61ccb975 100644 --- a/app/frontend/src/stylesheets/variables/layout.scss +++ b/app/frontend/src/stylesheets/variables/layout.scss @@ -29,10 +29,15 @@ } & > .content { + display: flex; + flex-direction: column; padding: 1.6rem; background-color: var(--gray-soft-light); border: 1px solid var(--gray-soft-dark); border-radius: var(--border-radius); + & > * { margin-bottom: 0; } + & > *:not(:last-child) { margin-bottom: 3.2rem; } + .fab-alert { margin: 0 0 1.6rem; } } @media (min-width: 1024px) { diff --git a/app/frontend/templates/admin/invoices/avoirModal.html b/app/frontend/templates/admin/invoices/avoirModal.html index 4e0192886..f49217f39 100644 --- a/app/frontend/templates/admin/invoices/avoirModal.html +++ b/app/frontend/templates/admin/invoices/avoirModal.html @@ -3,24 +3,6 @@
-
-
- -
-
- -
-
- -
-
+ diff --git a/app/frontend/templates/admin/plans/new.html b/app/frontend/templates/admin/plans/new.html index 9a2b14257..7315fe38b 100644 --- a/app/frontend/templates/admin/plans/new.html +++ b/app/frontend/templates/admin/plans/new.html @@ -14,14 +14,4 @@
-
-
- -
-
- -
-
- -
-
+ diff --git a/app/frontend/templates/admin/settings/reservations.html b/app/frontend/templates/admin/settings/reservations.html index 6f6469ba1..3307fedd9 100644 --- a/app/frontend/templates/admin/settings/reservations.html +++ b/app/frontend/templates/admin/settings/reservations.html @@ -108,9 +108,33 @@

{{ 'app.admin.settings.reservation_deadline' }}

{{ 'app.admin.settings.reservation_deadline_help' }}

- + + + + + +
-
- -
- - -
- {{ 'app.shared.wallet.creation_date_is_required' }} -
-

{{ 'app.shared.wallet.will_appear_on_the_refund_invoice' }}

diff --git a/app/helpers/db_helper.rb b/app/helpers/db_helper.rb new file mode 100644 index 000000000..abce80f1d --- /dev/null +++ b/app/helpers/db_helper.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Helpers for database operations +module DbHelper + # Ruby times are localised and does not have the same precision as database times do comparing them in .where() clauses may + # result in unexpected results. This function worksaround this issue by converting the Time to a database-comparable format + # @param [Time] + # @return [String] + def db_time(time) + time.utc.strftime('%Y-%m-%d %H:%M:%S.%6N') + end +end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index f5c89b72d..1520bd44d 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -58,7 +58,10 @@ module SettingsHelper space_explications_alert visibility_yearly visibility_others - reservation_deadline + machine_reservation_deadline + training_reservation_deadline + event_reservation_deadline + space_reservation_deadline display_name_enable machines_sort_by accounting_sales_journal_code diff --git a/app/models/avoir.rb b/app/models/avoir.rb index 0f28ce5dc..7a505aaea 100644 --- a/app/models/avoir.rb +++ b/app/models/avoir.rb @@ -12,6 +12,8 @@ class Avoir < Invoice attr_accessor :invoice_items_ids + delegate :order_number, to: :invoice + def generate_reference super(created_at) end diff --git a/app/models/cart_item/event_reservation.rb b/app/models/cart_item/event_reservation.rb index 122191c99..c2dd29824 100644 --- a/app/models/cart_item/event_reservation.rb +++ b/app/models/cart_item/event_reservation.rb @@ -71,4 +71,8 @@ class CartItem::EventReservation < CartItem::Reservation def total_tickets (normal_tickets || 0) + (cart_item_event_reservation_tickets.map(&:booked).reduce(:+) || 0) end + + def reservation_deadline_minutes + Setting.get('event_reservation_deadline').to_i + end end diff --git a/app/models/cart_item/machine_reservation.rb b/app/models/cart_item/machine_reservation.rb index 84db18ce4..9fccf8911 100644 --- a/app/models/cart_item/machine_reservation.rb +++ b/app/models/cart_item/machine_reservation.rb @@ -49,4 +49,8 @@ class CartItem::MachineReservation < CartItem::Reservation machine_credit = plan.machine_credits.find { |credit| credit.creditable_id == reservable.id } credits_hours(machine_credit, new_plan_being_bought: new_subscription) end + + def reservation_deadline_minutes + Setting.get('machine_reservation_deadline').to_i + end end diff --git a/app/models/cart_item/reservation.rb b/app/models/cart_item/reservation.rb index 372369fab..f7f38455e 100644 --- a/app/models/cart_item/reservation.rb +++ b/app/models/cart_item/reservation.rb @@ -13,18 +13,22 @@ class CartItem::Reservation < CartItem::BaseItem nil end + # @return [Plan,NilClass] def plan nil end + # @return [User] def operator operator_profile.user end + # @return [User] def customer customer_profile.user end + # @return [Hash{Symbol=>Integer,Hash{Symbol=>ArrayInteger,Float,Boolean,Time}>}}] def price is_privileged = operator.privileged? && operator.id != customer.id prepaid = { minutes: PrepaidPackService.minutes_available(customer, reservable) } @@ -48,49 +52,33 @@ class CartItem::Reservation < CartItem::BaseItem { elements: elements, amount: amount } end + # @return [String,NilClass] def name reservable&.name end + # @param all_items [Array] + # @return [Boolean] def valid?(all_items = []) pending_subscription = all_items.find { |i| i.is_a?(CartItem::Subscription) } + plan = pending_subscription&.plan || customer&.subscribed_plan - reservation_deadline_minutes = Setting.get('reservation_deadline').to_i - reservation_deadline = reservation_deadline_minutes.minutes.since + unless ReservationLimitService.authorized?(plan, customer, self, all_items) + errors.add(:reservation, I18n.t('cart_item_validation.limit_reached', { + HOURS: ReservationLimitService.limit(plan, reservable).limit, + RESERVABLE: reservable.name + })) + return false + end cart_item_reservation_slots.each do |sr| - slot = sr.slot - if slot.nil? - errors.add(:slot, I18n.t('cart_item_validation.slot')) - return false - end - - availability = slot.availability - if availability.nil? - errors.add(:availability, I18n.t('cart_item_validation.availability')) - return false - end - - if slot.full? - errors.add(:slot, I18n.t('cart_item_validation.full')) - return false - end - - if slot.start_at < reservation_deadline && !operator.privileged? - errors.add(:slot, I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes })) - return false - end - - next if availability.plan_ids.empty? - next if required_subscription?(availability, pending_subscription) - - errors.add(:availability, I18n.t('cart_item_validation.restricted')) - return false + return false unless validate_slot_reservation(sr, pending_subscription, errors) end true end + # @return [Reservation] def to_object ::Reservation.new( reservable_id: reservable_id, @@ -107,7 +95,7 @@ class CartItem::Reservation < CartItem::BaseItem end # Group the slots by date, if the extended_prices_in_same_day option is set to true - # @return Hash{Symbol => Array} + # @return [Hash{Symbol => Array}] def grouped_slots return { all: cart_item_reservation_slots } unless Setting.get('extended_prices_in_same_day') @@ -125,7 +113,7 @@ class CartItem::Reservation < CartItem::BaseItem # @option options [Boolean] :has_credits true if the user still has credits for the given slot, false if not provided # @option options [Boolean] :is_division false if the slot covers a full availability, true if it is a subdivision (default) # @option options [Number] :prepaid_minutes number of remaining prepaid minutes for the customer - # @return [Float] + # @return [Float,Integer] def get_slot_price_from_prices(prices, slot_reservation, is_privileged, options = {}) options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options) @@ -152,7 +140,7 @@ class CartItem::Reservation < CartItem::BaseItem # @option options [Boolean] :has_credits true if the user still has credits for the given slot, false if not provided # @option options [Boolean] :is_division false if the slot covers a full availability, true if it is a subdivision (default) # @option options [Number] :prepaid_minutes number of remaining prepaid minutes for the customer - # @return [Float] price of the slot + # @return [Float,Integer] price of the slot def get_slot_price(hourly_rate, slot_reservation, is_privileged, options = {}) options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options) @@ -245,14 +233,55 @@ class CartItem::Reservation < CartItem::BaseItem cart_item_reservation_slots.map { |sr| { id: sr.slots_reservation_id, slot_id: sr.slot_id, offered: sr.offered } } end - ## # Check if the given availability requires a valid subscription. If so, check if the current customer # has the required susbcription, otherwise, check if the operator is privileged - ## + # @param availability [Availability] + # @param pending_subscription [CartItem::Subscription, NilClass] def required_subscription?(availability, pending_subscription) (customer.subscribed_plan && availability.plan_ids.include?(customer.subscribed_plan.id)) || (pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) || (operator.manager? && customer.id != operator.id) || operator.admin? end + + # Gets the deadline in minutes for slots in this reservation + # @return [Integer] + def reservation_deadline_minutes + 0 + end + + # @param reservation_slot [CartItem::ReservationSlot] + # @param pending_subscription [CartItem::Subscription, NilClass] + # @param errors [ActiveModel::Errors] + # @return [Boolean] + def validate_slot_reservation(reservation_slot, pending_subscription, 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.minutes.since && !operator.privileged? + errors.add(:slot, I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes })) + 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 diff --git a/app/models/cart_item/space_reservation.rb b/app/models/cart_item/space_reservation.rb index d8859504d..aa513c3b8 100644 --- a/app/models/cart_item/space_reservation.rb +++ b/app/models/cart_item/space_reservation.rb @@ -34,4 +34,8 @@ class CartItem::SpaceReservation < CartItem::Reservation space_credit = plan.space_credits.find { |credit| credit.creditable_id == reservable.id } credits_hours(space_credit, new_plan_being_bought: new_subscription) end + + def reservation_deadline_minutes + Setting.get('space_reservation_deadline').to_i + end end diff --git a/app/models/cart_item/training_reservation.rb b/app/models/cart_item/training_reservation.rb index 4ebeaf040..aff64b3a6 100644 --- a/app/models/cart_item/training_reservation.rb +++ b/app/models/cart_item/training_reservation.rb @@ -45,4 +45,8 @@ class CartItem::TrainingReservation < CartItem::Reservation is_creditable = plan&.training_credits&.select { |credit| credit.creditable_id == reservable&.id }&.any? is_creditable ? plan&.training_credit_nb : 0 end + + def reservation_deadline_minutes + Setting.get('training_reservation_deadline').to_i + end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 40a6b1a20..a8a54844f 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -14,8 +14,9 @@ class Invoice < PaymentDocument belongs_to :coupon has_one :avoir, class_name: 'Invoice', dependent: :destroy, inverse_of: :avoir - has_one :payment_schedule_item, dependent: :nullify + has_one :payment_schedule_item, dependent: :restrict_with_error has_one :payment_gateway_object, as: :item, dependent: :destroy + has_one :order, dependent: :restrict_with_error belongs_to :operator_profile, class_name: 'InvoicingProfile' has_many :accounting_lines, dependent: :destroy @@ -49,6 +50,12 @@ class Invoice < PaymentDocument end def order_number + return order.reference unless order.nil? + + if !payment_schedule_item.nil? && !payment_schedule_item.first? + return payment_schedule_item.payment_schedule.ordered_items.first.invoice.order_number + end + PaymentDocumentService.generate_order_number(self) end @@ -66,8 +73,7 @@ class Invoice < PaymentDocument avoir.attributes = attrs avoir.reference = nil avoir.invoice_id = id - # override created_at to compute CA in stats - avoir.created_at = avoir.avoir_date + avoir.avoir_date = Time.current avoir.total = 0 # refunds of invoices with cash coupons: we need to ventilate coupons on paid items paid_items = 0 @@ -79,7 +85,6 @@ class Invoice < PaymentDocument refund_items += 1 unless ii.amount.zero? avoir_ii = avoir.invoice_items.build(ii.dup.attributes) - avoir_ii.created_at = avoir.avoir_date avoir_ii.invoice_item_id = ii.id avoir.total += avoir_ii.amount end diff --git a/app/models/machine.rb b/app/models/machine.rb index bd4e71854..a3f81cfa1 100644 --- a/app/models/machine.rb +++ b/app/models/machine.rb @@ -42,6 +42,8 @@ class Machine < ApplicationRecord belongs_to :machine_category + has_many :plan_limitations, dependent: :destroy, inverse_of: :machine, foreign_type: 'limitable_type', foreign_key: 'limitable_id' + after_create :create_statistic_subtype after_create :create_machine_prices after_create :update_gateway_product diff --git a/app/models/machine_category.rb b/app/models/machine_category.rb index 9fe2e9de0..47c670640 100644 --- a/app/models/machine_category.rb +++ b/app/models/machine_category.rb @@ -4,4 +4,5 @@ class MachineCategory < ApplicationRecord has_many :machines, dependent: :nullify accepts_nested_attributes_for :machines, allow_destroy: true + has_many :plan_limitations, dependent: :destroy, inverse_of: :machine_category, foreign_type: 'limitable_type', foreign_key: 'limitable_id' end diff --git a/app/models/plan.rb b/app/models/plan.rb index d3b8b6e39..0a508f8e2 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -21,6 +21,9 @@ class Plan < ApplicationRecord has_many :cart_item_subscriptions, class_name: 'CartItem::Subscription', dependent: :destroy has_many :cart_item_payment_schedules, class_name: 'CartItem::PaymentSchedule', dependent: :destroy + has_many :plan_limitations, dependent: :destroy + accepts_nested_attributes_for :plan_limitations, allow_destroy: true + extend FriendlyId friendly_id :base_name, use: :slugged diff --git a/app/models/plan_limitation.rb b/app/models/plan_limitation.rb new file mode 100644 index 000000000..9bdabecbf --- /dev/null +++ b/app/models/plan_limitation.rb @@ -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] + def reservables + return limitable.machines if limitable_type == 'MachineCategory' + + [limitable] + end +end diff --git a/app/models/reservation.rb b/app/models/reservation.rb index 50f240627..d0472354b 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -32,6 +32,7 @@ class Reservation < ApplicationRecord after_commit :notify_member_create_reservation, on: :create after_commit :notify_admin_member_create_reservation, on: :create after_commit :extend_subscription, on: :create + after_commit :notify_member_limitation_reached, on: :create delegate :user, to: :statistic_profile @@ -137,4 +138,14 @@ class Reservation < ApplicationRecord receiver: User.admins_and_managers, attached_object: self end + + def notify_member_limitation_reached + date = ReservationLimitService.reached_limit_date(self) + return if date.nil? + + NotificationCenter.call type: 'notify_member_reservation_limit_reached', + receiver: user, + attached_object: ReservationLimitService.limit(user.subscribed_plan, reservable), + meta_data: { date: date } + end end diff --git a/app/models/shopping_cart.rb b/app/models/shopping_cart.rb index 01b2c6046..31ed97e43 100644 --- a/app/models/shopping_cart.rb +++ b/app/models/shopping_cart.rb @@ -160,6 +160,7 @@ class ShoppingCart # Check if the current cart needs the user to have been validated, and if the condition is satisfied. # Return an array of errors, if any; false otherwise + # @return [Array,FalseClass] def check_user_validation(items) user_validation_required = Setting.get('user_validation_required') user_validation_required_list = Setting.get('user_validation_required_list') diff --git a/app/services/availabilities/availabilities_service.rb b/app/services/availabilities/availabilities_service.rb index 9ab7388e6..d2fcd3f96 100644 --- a/app/services/availabilities/availabilities_service.rb +++ b/app/services/availabilities/availabilities_service.rb @@ -6,11 +6,6 @@ class Availabilities::AvailabilitiesService # @param level [String] 'slot' | 'availability' def initialize(current_user, level = 'slot') @current_user = current_user - @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 @level = level end @@ -119,33 +114,23 @@ class Availabilities::AvailabilitiesService end # @param availabilities [ActiveRecord::Relation] - # @param type [String] + # @param type [String] 'training', 'space', 'machines' or 'event' # @param user [User] # @param range_start [ActiveSupport::TimeWithZone] # @param range_end [ActiveSupport::TimeWithZone] # @return ActiveRecord::Relation def availabilities(availabilities, type, user, range_start, range_end) - # who made the request? - # 1) an admin (he can see all availabilities from 1 month ago to anytime in the future) - if @current_user&.admin? || @current_user&.manager? - window_start = [range_start, 1.month.ago].max - availabilities.includes(:tags, :slots) - .joins(:slots) - .where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', range_end, window_start, type) - .where('slots.start_at > ? AND slots.end_at < ?', window_start, range_end) - # 2) an user (he cannot see past availabilities neither those further than 1 (or 3) months in the future) - else - end_at = @maximum_visibility[:other] - end_at = @maximum_visibility[:year] if subscription_year?(user) && type != 'training' - end_at = @maximum_visibility[:year] if show_more_trainings?(user) && type == 'training' - window_end = [end_at, range_end].min - window_start = [range_start, @minimum_visibility].max - availabilities.includes(:tags, :slots) - .joins(:slots) - .where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', window_end, window_start, type) - .where('slots.start_at > ? AND slots.end_at < ?', window_start, window_end) - .where('availability_tags.tag_id' => user&.tag_ids&.concat([nil])) - .where(lock: false) + window = Availabilities::VisibilityService.new.visibility(@current_user, type, range_start, range_end) + qry = availabilities.includes(:tags, :slots) + .joins(:slots) + .where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', window[1], window[0], type) + .where('slots.start_at > ? AND slots.end_at < ?', window[0], window[1]) + unless @current_user&.privileged? + # non priviledged users cannot see availabilities with tags different than their own and locked tags + qry = qry.where('availability_tags.tag_id' => user&.tag_ids&.concat([nil])) + .where(lock: false) end + + qry end end diff --git a/app/services/availabilities/visibility_service.rb b/app/services/availabilities/visibility_service.rb new file mode 100644 index 000000000..21da2b8c1 --- /dev/null +++ b/app/services/availabilities/visibility_service.rb @@ -0,0 +1,72 @@ +# 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 = { + machine: Setting.get('machine_reservation_deadline').to_i.minutes.since, + training: Setting.get('training_reservation_deadline').to_i.minutes.since, + event: Setting.get('event_reservation_deadline').to_i.minutes.since, + space: Setting.get('space_reservation_deadline').to_i.minutes.since + } + end + + # @param user [User,NilClass] + # @param available_type [String] 'training', 'space', 'machines' or 'event' + # @param range_start [ActiveSupport::TimeWithZone] + # @param range_end [ActiveSupport::TimeWithZone] + # @return [Array] 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 + + minimum_visibility = 0.minutes.since + minimum_visibility = @minimum_visibility[:machine] if available_type == 'machines' + minimum_visibility = @minimum_visibility[:training] if available_type == 'training' + minimum_visibility = @minimum_visibility[:event] if available_type == 'event' + minimum_visibility = @minimum_visibility[:space] if available_type == 'space' + + 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 == 'machines' + + 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 diff --git a/app/services/cart_service.rb b/app/services/cart_service.rb index aea1c834d..6c6588e4b 100644 --- a/app/services/cart_service.rb +++ b/app/services/cart_service.rb @@ -6,10 +6,9 @@ class CartService @operator = operator end - ## # For details about the expected hash format # @see app/frontend/src/javascript/models/payment.ts > interface ShoppingCart - ## + # @return [ShoppingCart] def from_hash(cart_items) cart_items.permit! if cart_items.is_a? ActionController::Parameters diff --git a/app/services/payment_document_service.rb b/app/services/payment_document_service.rb index c4ac219af..e5436099b 100644 --- a/app/services/payment_document_service.rb +++ b/app/services/payment_document_service.rb @@ -3,10 +3,13 @@ # Provides methods to generate Invoice, Avoir or PaymentSchedule references class PaymentDocumentService class << self + include DbHelper + # @param document [PaymentDocument] + # @param date [Time] def generate_reference(document, date: Time.current) - pattern = Setting.get('invoice_reference') + pattern = Setting.get('invoice_reference').to_s - reference = replace_invoice_number_pattern(pattern, document.created_at) + reference = replace_document_number_pattern(pattern, document, document.created_at) reference = replace_date_pattern(reference, date) case document @@ -44,15 +47,16 @@ class PaymentDocumentService reference end + # @param document [PaymentDocument] def generate_order_number(document) pattern = Setting.get('invoice_order-nb') # global document number (nn..nn) reference = pattern.gsub(/n+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices(document.is_a?(Order) ? 'order' : 'global'), match.to_s.length) + pad_and_truncate(number_of_order('global', document, document.created_at), match.to_s.length) end - reference = replace_invoice_number_pattern(reference, document.created_at) + reference = replace_document_number_pattern(reference, document, document.created_at, :number_of_order) replace_date_pattern(reference, document.created_at) end @@ -69,25 +73,55 @@ class PaymentDocumentService # Returns the number of current invoices in the given range around the current date. # If range is invalid or not specified, the total number of invoices is returned. # @param range [String] 'day', 'month', 'year' - # @param date [Date] the ending date + # @param document [PaymentDocument] + # @param date [Time] the ending date # @return [Integer] - def number_of_invoices(range, date = Time.current) - case range.to_s - when 'day' - start = date.beginning_of_day - when 'month' - start = date.beginning_of_month - when 'year' - start = date.beginning_of_year - else - return get_max_id(Invoice) + get_max_id(PaymentSchedule) + get_max_id(Order) - end + def number_of_documents(range, document, date = Time.current) + start = case range.to_s + when 'day' + date.beginning_of_day + when 'month' + date.beginning_of_month + when 'year' + date.beginning_of_year + else + nil + end ending = date - return Invoice.count + PaymentSchedule.count + Order.count unless defined? start - Invoice.where('created_at >= :start_date AND created_at <= :end_date', start_date: start, end_date: ending).length + - PaymentSchedule.where('created_at >= :start_date AND created_at <= :end_date', start_date: start, end_date: ending).length + - Order.where('created_at >= :start_date AND created_at <= :end_date', start_date: start, end_date: ending).length + documents = document.class.base_class + .where('created_at <= :end_date', end_date: db_time(ending)) + + documents = documents.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil? + + documents.count + end + + def number_of_order(range, _document, date = Time.current) + start = case range.to_s + when 'day' + date.beginning_of_day + when 'month' + date.beginning_of_month + when 'year' + date.beginning_of_year + else + nil + end + ending = date + orders = Order.where('created_at <= :end_date', end_date: db_time(ending)) + orders = orders.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil? + + schedules = PaymentSchedule.where('created_at <= :end_date', end_date: db_time(ending)) + schedules = schedules.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil? + + invoices = Invoice.where(type: nil) + .where.not(id: orders.map(&:invoice_id)) + .where.not(id: schedules.map(&:payment_schedule_items).flatten.map(&:invoice_id).filter(&:present?)) + .where('created_at <= :end_date', end_date: db_time(ending)) + invoices = invoices.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil? + + orders.count + schedules.count + invoices.count end # Replace the date elements in the provided pattern with the date values, from the provided date @@ -102,7 +136,9 @@ class PaymentDocumentService copy.gsub!(/(?![^\[]*\])YY(?![^\[]*\])/, date.strftime('%y')) # abbreviated month name (MMM) - copy.gsub!(/(?![^\[]*\])MMM(?![^\[]*\])/, date.strftime('%^b')) + # we cannot replace by the month name directly because it may contrains an M or a D + # so we replace it by a special indicator and, at the end, we will replace it by the abbreviated month name + copy.gsub!(/(?![^\[]*\])MMM(?![^\[]*\])/, '}~{') # month of the year, zero-padded (MM) copy.gsub!(/(?![^\[]*\])MM(?![^\[]*\])/, date.strftime('%m')) # month of the year, non zero-padded (M) @@ -110,40 +146,37 @@ class PaymentDocumentService # day of the month, zero-padded (DD) copy.gsub!(/(?![^\[]*\])DD(?![^\[]*\])/, date.strftime('%d')) - # day of the month, non zero-padded (DD) - copy.gsub!(/(?![^\[]*\])DD(?![^\[]*\])/, date.strftime('%-d')) + # day of the month, non zero-padded (D) + copy.gsub!(/(?![^\[]*\])D(?![^\[]*\])/, date.strftime('%-d')) + + # abbreviated month name (MMM) (2) + copy.gsub!(/(?![^\[]*\])}~\{(?![^\[]*\])/, date.strftime('%^b')) copy end # Replace the document number elements in the provided pattern with counts from the database # @param reference [String] + # @param document [PaymentDocument] # @param date [Time] - def replace_invoice_number_pattern(reference, date) + # @param count_method [Symbol] :number_of_documents OR :number_of_order + def replace_document_number_pattern(reference, document, date, count_method = :number_of_documents) copy = reference.dup # document number per year (yy..yy) copy.gsub!(/y+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('year', date), match.to_s.length) + pad_and_truncate(send(count_method, 'year', document, date), match.to_s.length) end # document number per month (mm..mm) copy.gsub!(/m+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('month', date), match.to_s.length) + pad_and_truncate(send(count_method, 'month', document, date), match.to_s.length) end # document number per day (dd..dd) copy.gsub!(/d+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('day', date), match.to_s.length) + pad_and_truncate(send(count_method, 'day', document, date), match.to_s.length) end copy end - - ## - # Return the maximum ID from the database, for the given class - # @param klass {ActiveRecord::Base} - ## - def get_max_id(klass) - ActiveRecord::Base.connection.execute("SELECT max(id) FROM #{klass.table_name}").getvalue(0, 0) || 0 - end end end diff --git a/app/services/prepaid_pack_service.rb b/app/services/prepaid_pack_service.rb index 9e0437e96..7a03ecaef 100644 --- a/app/services/prepaid_pack_service.rb +++ b/app/services/prepaid_pack_service.rb @@ -73,7 +73,7 @@ class PrepaidPackService # Total number of prepaid minutes available # @param user [User] - # @param priceable [Machine,Space] + # @param priceable [Machine,Space,NilClass] def minutes_available(user, priceable) return 0 if Setting.get('pack_only_for_subscription') && !user.subscribed_plan diff --git a/app/services/reservation_limit_service.rb b/app/services/reservation_limit_service.rb new file mode 100644 index 000000000..64ae9b7dc --- /dev/null +++ b/app/services/reservation_limit_service.rb @@ -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] + # @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] + # @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 diff --git a/app/services/trainings/auto_cancel_service.rb b/app/services/trainings/auto_cancel_service.rb index dc6e25f72..0e28bb6a8 100644 --- a/app/services/trainings/auto_cancel_service.rb +++ b/app/services/trainings/auto_cancel_service.rb @@ -80,7 +80,7 @@ class Trainings::AutoCancelService service = WalletService.new(user: reservation.user, wallet: reservation.user.wallet) transaction = service.credit(amount) - service.create_avoir(transaction, Time.current, I18n.t('trainings.refund_for_auto_cancel')) if transaction + service.create_avoir(transaction, I18n.t('trainings.refund_for_auto_cancel')) if transaction end end end diff --git a/app/services/wallet_service.rb b/app/services/wallet_service.rb index 6dc4dbcdd..654cf0957 100644 --- a/app/services/wallet_service.rb +++ b/app/services/wallet_service.rb @@ -50,11 +50,10 @@ class WalletService end ## create a refund invoice associated with the given wallet transaction - def create_avoir(wallet_transaction, avoir_date, description) + def create_avoir(wallet_transaction, description) avoir = Avoir.new avoir.type = 'Avoir' - avoir.avoir_date = avoir_date - avoir.created_at = avoir_date + avoir.avoir_date = Time.current avoir.description = description avoir.payment_method = 'wallet' avoir.subscription_to_expire = false diff --git a/app/views/api/notifications/_notify_member_reservation_limit_reached.json.jbuilder b/app/views/api/notifications/_notify_member_reservation_limit_reached.json.jbuilder new file mode 100644 index 000000000..f90b16e39 --- /dev/null +++ b/app/views/api/notifications/_notify_member_reservation_limit_reached.json.jbuilder @@ -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)) diff --git a/app/views/api/plans/_plan.json.jbuilder b/app/views/api/plans/_plan.json.jbuilder index ddffd567c..30e090760 100644 --- a/app/views/api/plans/_plan.json.jbuilder +++ b/app/views/api/plans/_plan.json.jbuilder @@ -1,7 +1,7 @@ # frozen_string_literal: true json.extract! plan, :id, :base_name, :name, :interval, :interval_count, :group_id, :training_credit_nb, :is_rolling, :description, :type, - :ui_weight, :disabled, :monthly_payment, :plan_category_id + :ui_weight, :disabled, :monthly_payment, :plan_category_id, :limiting, :machines_visibility json.amount plan.amount / 100.00 json.prices_attributes plan.prices, partial: 'api/prices/price', as: :price if plan.plan_file @@ -27,3 +27,7 @@ if plan.advanced_accounting end end +json.plan_limitations_attributes plan.plan_limitations do |limitation| + json.extract! limitation, :id, :limitable_id, :limitable_type, :limit +end + diff --git a/app/views/api/plans/show.json.jbuilder b/app/views/api/plans/show.json.jbuilder index 799730b4d..edba9aaf4 100644 --- a/app/views/api/plans/show.json.jbuilder +++ b/app/views/api/plans/show.json.jbuilder @@ -1 +1,3 @@ +# frozen_string_literal: true + json.partial! 'api/plans/plan', plan: @plan diff --git a/app/views/api/prices/compute.json.jbuilder b/app/views/api/prices/compute.json.jbuilder index 1548ea1d0..1aeb2b86a 100644 --- a/app/views/api/prices/compute.json.jbuilder +++ b/app/views/api/prices/compute.json.jbuilder @@ -21,3 +21,4 @@ if @amount[:schedule] end end end +json.errors @errors diff --git a/app/views/notifications_mailer/notify_member_reservation_limit_reached.html.erb b/app/views/notifications_mailer/notify_member_reservation_limit_reached.html.erb new file mode 100644 index 000000000..e8949e66d --- /dev/null +++ b/app/views/notifications_mailer/notify_member_reservation_limit_reached.html.erb @@ -0,0 +1,9 @@ +<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> + +

+ <%= t('.body.limit_reached', + HOURS: @attached_object.limit, + ITEM: @attached_object.limitable.name, + DATE: I18n.l(@notification.get_meta_data(:date).to_date)) %> +

+ diff --git a/config/locales/app.admin.de.yml b/config/locales/app.admin.de.yml index d8892dab7..55b1c5d0e 100644 --- a/config/locales/app.admin.de.yml +++ b/config/locales/app.admin.de.yml @@ -2,8 +2,8 @@ de: app: admin: edit_destroy_buttons: - deleted: "The {TYPE} was successfully deleted." - unable_to_delete: "Unable to delete the {TYPE}: " + deleted: "Successfully deleted." + unable_to_delete: "Unable to delete: " delete_item: "Delete the {TYPE}" confirm_delete: "Delete" delete_confirmation: "Are you sure you want to delete this {TYPE}?" @@ -24,7 +24,7 @@ de: save: "Save" successfully_saved: "Your banner was successfully saved." machine_categories_list: - machine_categories: "Machines categories" + machine_categories: "Maschinen-Kategorien" add_a_machine_category: "Add a machine category" name: "Name" machines_number: "Number of machines" @@ -57,7 +57,7 @@ de: disabled_help: "When disabled, the machine won't be reservable and won't appear by default in the machines list." reservable: "Can this machine be reserved?" reservable_help: "When disabled, the machine will be shown in the default list of machines, but without the reservation button. If you already have created some availability slots for this machine, you may want to remove them: do it from the admin agenda." - save: "Save" + save: "Speichern" create_success: "The machine was created successfully" update_success: "The machine was updated successfully" training_form: @@ -74,7 +74,7 @@ de: associated_machines_help: "If you associate a machine to this training, the members will need to successfully pass this training before being able to reserve the machine." default_seats: "Default number of seats" public_page: "Show in training lists" - public_help: "When unchecked, this option will prevent the training from appearing in the trainings list." + public_help: "Wenn diese Option deaktiviert ist, wird verhindert, dass das Training in der Trainingliste erscheint." disable_training: "Disable the training" disabled_help: "When disabled, the training won't be reservable and won't appear by default in the trainings list." automatic_cancellation: "Automatic cancellation" @@ -90,39 +90,39 @@ de: validation_rule_info: "Define a rule that cancel an authorisation if the machines associated with the training are not reserved for a specific period of time. This rule prevails over the authorisations validity period." validation_rule_switch: "Activate the validation rule" validation_rule_period: "Time limit in months" - save: "Save" - create_success: "The training was created successfully" - update_success: "The training was updated successfully" + save: "Speichern" + create_success: "Die Schulung wurde erfolgreich erstellt" + update_success: "Die Schulung wurde erfolgreich aktualisiert" space_form: - ACTION_title: "{ACTION, select, create{New} other{Update the}} space" - watch_out_when_creating_a_new_space_its_prices_are_initialized_at_0_for_all_subscriptions: "Watch out! When creating a new space, its prices are initialized at 0 for all subscriptions." - consider_changing_its_prices_before_creating_any_reservation_slot: "Consider changing its prices before creating any reservation slot." + ACTION_title: "{ACTION, select, create{Neu} other{Aktualisiere den}} Raum" + watch_out_when_creating_a_new_space_its_prices_are_initialized_at_0_for_all_subscriptions: "Achtung! Beim Erstellen eines neuen Raums wird sein Preis für alle Abonnements mit 0 angelegt." + consider_changing_its_prices_before_creating_any_reservation_slot: "Ändern Sie ggf. die Preise, bevor Sie Reservierungs-Slots erstellen." name: "Name" - illustration: "Illustration" - description: "Description" - characteristics: "Characteristics" - attachments: "Attachments" - attached_files_pdf: "Attached files (pdf)" - add_an_attachment: "Add an attachment" - settings: "Settings" - default_seats: "Default number of seats" - disable_space: "Disable the space" - disabled_help: "When disabled, the space won't be reservable and won't appear by default in the spaces list." - save: "Save" - create_success: "The space was created successfully" - update_success: "The space was updated successfully" + illustration: "Abbildung" + description: "Beschreibung" + characteristics: "Eigenschaften" + attachments: "Dateianhänge" + attached_files_pdf: "Angehängte Dateien (pdf)" + add_an_attachment: "Anhang hinzufügen" + settings: "Einstellungen" + default_seats: "Standardanzahl der Sitze" + disable_space: "Raum deaktivieren" + disabled_help: "Wenn deaktiviert, ist der Raum nicht reservierbar und erscheint standardmäßig nicht in der Liste der Leerzeichen." + save: "Speichern" + create_success: "Der Raum wurde erfolgreich erstellt" + update_success: "Der Raum wurde erfolgreich aktualisiert" event_form: - ACTION_title: "{ACTION, select, create{New} other{Update the}} event" - title: "Title" + ACTION_title: "{ACTION, select, create{Neue} other{Aktualisiere die}} Veranstaltung" + title: "Titel" matching_visual: "Matching visual" description: "Description" attachments: "Attachments" attached_files_pdf: "Attached files (pdf)" add_a_new_file: "Add a new file" - event_category: "Event category" + event_category: "Veranstaltungskategorie" dates_and_opening_hours: "Dates and opening hours" all_day: "All day" - all_day_help: "Will the event last all day or do you want to set times?" + all_day_help: "Wird das Ereignis den ganzen Tag dauern oder möchtest du Zeiten festlegen?" start_date: "Start date" end_date: "End date" start_time: "Start time" @@ -135,29 +135,36 @@ de: fare_class: "Fare class" price: "Price" seats_available: "Seats available" - seats_help: "If you leave this field empty, this event will be available without reservations." - event_themes: "Event themes" + seats_help: "Wenn sie dieses Feld leer lassen, ist diese Veranstaltung ohne Reservierung." + event_themes: "Veranstaltungsthemen" age_range: "Age range" add_price: "Add a price" save: "Save" - create_success: "The event was created successfully" - events_updated: "{COUNT, plural, =1{One event was} other{{COUNT} Events were}} successfully updated" - events_not_updated: "{TOTAL, plural, =1{The event was} other{On {TOTAL} events {COUNT, plural, =1{one was} other{{COUNT} were}}}} not updated." - error_deleting_reserved_price: "Unable to remove the requested price because it is associated with some existing reservations" - other_error: "An unexpected error occurred while updating the event" + create_success: "Die Veranstaltung wurde erfolgreich erstellt" + events_updated: "{COUNT, plural, one {}=1{Eine Veranstaltung wurde} other{{COUNT} Veranstaltungen wurden}} erfolgreich aktualisiert" + events_not_updated: "{TOTAL, plural, =1{Die Veranstaltung war} other{Auf {TOTAL} Veranstaltungen {COUNT, plural, =1{eins war} other{{COUNT} waren}}}} nicht aktualisiert." + error_deleting_reserved_price: "Der angeforderte Preis konnte nicht gelöscht werden, da er mit einigen Reservierungen verknüpft ist" + other_error: "Beim Aktualisieren der Veranstaltung ist ein unerwarteter Fehler aufgetreten" recurring: none: "None" - every_days: "Every days" - every_week: "Every week" - every_month: "Every month" - every_year: "Every year" + every_days: "Täglich" + every_week: "Wöchentlich" + every_month: "Monatlich" + every_year: "Jährlich" plan_form: - general_information: "General information" + ACTION_title: "{ACTION, select, create{New} other{Update the}} plan" + tab_settings: "Einstellungen" + tab_usage_limits: "Usage limits" + description: "Beschreibung" + general_settings: "Generelle Einstellungen" + general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods." + activation_and_payment: "Subscription activation and payment" name: "Name" name_max_length: "Name length must be less than 24 characters." - group: "Group" + group: "Gruppe" transversal: "Transversal plan" transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups." + display: "Display" category: "Category" category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions." number_of_periods: "Number of periods" @@ -173,10 +180,9 @@ de: rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought." monthly_payment: "Monthly payment?" monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months." - description: "Description" information_sheet: "Information sheet" notified_partner: "Notified partner" - new_user: "New user ..." + new_user: "New user" alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user." disabled: "Disable subscription" disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it." @@ -185,9 +191,40 @@ de: partner_plan: "Partner plan" partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan." partner_created: "The partner was successfully created" - ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan" + slots_visibility: "Slots visibility" + slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings." + machines_visibility: "Visibility time limit, in hours (machines)" + visibility_minimum: "Visibility cannot be less than 7 hours" + save: "Save" create_success: "Plan(s) successfully created. Don't forget to redefine prices." update_success: "The plan was updated successfully" + plan_limit_form: + usage_limitation: "Limitation of use" + usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation." + usage_limitation_switch: "Restrict machine reservations to a number of hours per day." + new_usage_limitation: "Add a limitation of use" + all_limitations: "All limitations" + by_category: "By machines category" + by_machine: "By machine" + category: "Machines category" + machine: "Machine name" + max_hours_per_day: "Max. hours/day" + ongoing_limitations: "Ongoing limitations" + saved_limitations: "Saved limitations" + cancel: "Cancel this limitation" + cancel_deletion: "Cancel" + ongoing_deletion: "Ongoing deletion" + plan_limit_modal: + title: "Manage limitation of use" + limit_reservations: "Limit reservations" + by_category: "By machines category" + by_machine: "By machine" + category: "Machines category" + machine: "Machine name" + categories_info: "If you select all machine categories, the limits will apply across the board." + machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten." + max_hours_per_day: "Maximum number of reservation hours per day" + confirm: "Confirm" partner_modal: title: "Create a new partner" create_partner: "Create the partner" @@ -196,16 +233,17 @@ de: email: "Email address" plan_pricing_form: prices: "Prices" + about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour." copy_prices_from: "Copy prices from" copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan" machines: "Machines" spaces: "Spaces" update_recurrent_modal: title: "Periodic event update" - edit_recurring_event: "You're about to update a periodic event. What do you want to update?" - edit_this_event: "Only this event" + edit_recurring_event: "Sie bearbeiten eine wiederkehrende Veranstaltung. Was möchten Sie ändern?" + edit_this_event: "Nur diese Veranstaltung" edit_this_and_next: "This event and the followings" - edit_all: "All events" + edit_all: "Alle Veranstaltungen" date_wont_change: "Warning: you have changed the event date. This modification won't be propagated to other occurrences of the periodic event." confirm: "Update the {MODE, select, single{event} other{events}}" advanced_accounting_form: @@ -227,7 +265,7 @@ de: subscriptions: "Subscriptions" machine: "Machine reservation" training: "Training reservation" - event: "Event reservation" + event: "Veranstaltungsreservierung" space: "Space reservation" prepaid_pack: "Pack of prepaid-hours" product: "Product of the store" @@ -286,7 +324,7 @@ de: manage_trainings: "Klicke hier, um Schulungen hinzuzufügen oder zu entfernen." number_of_tickets: "Anzahl der Tickets: " adjust_the_opening_hours: "Öffnungszeiten anpassen" - to_time: "bis" #eg. from 18:00 to 21:00 + to_time: "bis" #e.g. from 18:00 to 21:00 restrict_options: "Einschränkungsoptionen" restrict_with_labels: "Diesen Slot mit Labels einschränken" restrict_for_subscriptions: "Diesen Slot auf Abonnenten einschränken" @@ -508,8 +546,8 @@ de: on_DATE: "am {DATE}" from_DATE: "von {DATE}" from_TIME: "ab {TIME}" - to_date: "bis" #eg: from 01/01 to 01/05 - to_time: "bis" #eg. from 18:00 to 21:00 + to_date: "bis" #e.g.: from 01/01 to 01/05 + to_time: "bis" #e.g. from 18:00 to 21:00 title: "Titel" dates: "Datum" booking: "Buchung" @@ -586,13 +624,13 @@ de: events_settings: title: "Settings" generic_text_block: "Editorial text block" - generic_text_block_info: "Displays an editorial block above the list of events visible to members." + generic_text_block_info: "Zeigt einen redaktionellen Block oberhalb der für Mitglieder sichtbaren Veranstaltungsliste." generic_text_block_switch: "Display editorial block" cta_switch: "Display a button" cta_label: "Button label" cta_url: "url" save: "Save" - update_success: "The events settings were successfully updated" + update_success: "Die Einstellungen wurden erfolgreich aktualisiert" #subscriptions, prices, credits and coupons management pricing: pricing_management: "Preisverwaltung" @@ -885,8 +923,6 @@ de: deleted_user: "Gelöschter Nutzer" refund_invoice_successfully_created: "Rückerstattungsrechnung erfolgreich erstellt." create_a_refund_on_this_invoice: "Erstelle eine Rückerstattung mit dieser Rechnung" - creation_date_for_the_refund: "Erstellungsdatum für die Erstattung" - creation_date_is_required: "Erstellungsdatum ist erforderlich." refund_mode: "Erstattungsmodus:" do_you_want_to_disable_the_user_s_subscription: "Möchten Sie das Abonnement des Benutzers deaktivieren:" elements_to_refund: "Erstattungselemente" @@ -1453,8 +1489,8 @@ de: statistics: "Statistiken" evolution: "Entwicklung" age_filter: "Altersfilter" - from_age: "Von" #eg. from 8 to 40 years old - to_age: "bis" #eg. from 8 to 40 years old + from_age: "Von" #e.g. from 8 to 40 years old + to_age: "bis" #e.g. from 8 to 40 years old start: "Start:" end: "Ende:" custom_filter: "Benutzerderfinierter Filter" @@ -1572,7 +1608,10 @@ de: visibility_for_other_members: "Für alle anderen Mitglieder" reservation_deadline: "Prevent last minute booking" reservation_deadline_help: "If you increase the prior period, members won't be able to book a slot X minutes before its start." - deadline_minutes: "Prior period (minutes)" + machine_deadline_minutes: "Machine prior period (minutes)" + training_deadline_minutes: "Training prior period (minutes)" + event_deadline_minutes: "Event prior period (minutes)" + space_deadline_minutes: "Space prior period (minutes)" ability_for_the_users_to_move_their_reservations: "Möglichkeit für die Benutzer, ihre Reservierungen zu verschieben" reservations_shifting: "Verschiebung von Reservierungen" prior_period_hours: "Vorheriger Zeitraum (Stunden)" @@ -2215,8 +2254,6 @@ de: unexpected_error_occurred: "An unexpected error occurred. Please try again later." all_products: "All products" create_a_product: "Create a product" - successfully_deleted: "The product has been successfully deleted" - unable_to_delete: "Unable to delete the product: " filter: "Filter" filter_clear: "Clear all" filter_apply: "Apply" @@ -2238,6 +2275,7 @@ de: sort: "Sort:" visible_only: "Visible products only" product_item: + product: "product" visible: "visible" hidden: "hidden" stock: @@ -2288,12 +2326,13 @@ de: low_stock: "Low stock" threshold_level: "Minimum threshold level" threshold_alert: "Notify me when the threshold is reached" - events_history: "Events history" - event_type: "Events:" + events_history: "Veranstaltungsverlauf" + event_type: "Veranstaltungen:" reason: "Reason" stocks: "Stock:" internal: "Private stock" external: "Public stock" + edit: "Edit" all: "All types" remaining_stock: "Remaining stock" type_in: "Add" @@ -2385,7 +2424,7 @@ de: VAT_rate_machine: "Machine reservation" VAT_rate_space: "Space reservation" VAT_rate_training: "Training reservation" - VAT_rate_event: "Event reservation" + VAT_rate_event: "Veranstaltungsreservierung" VAT_rate_subscription: "Subscription" VAT_rate_product: "Products (store)" multi_VAT_notice: "Please note: The current general rate is {RATE}%. You can define different VAT rates for each category.

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." diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 7ca09e20c..f115a96ad 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -2,8 +2,8 @@ en: app: admin: edit_destroy_buttons: - deleted: "The {TYPE} was successfully deleted." - unable_to_delete: "Unable to delete the {TYPE}: " + deleted: "Successfully deleted." + unable_to_delete: "Unable to delete: " delete_item: "Delete the {TYPE}" confirm_delete: "Delete" delete_confirmation: "Are you sure you want to delete this {TYPE}?" @@ -152,12 +152,19 @@ en: every_month: "Every month" every_year: "Every year" plan_form: - general_information: "General information" + ACTION_title: "{ACTION, select, create{New} other{Update the}} plan" + tab_settings: "Settings" + tab_usage_limits: "Usage limits" + description: "Description" + general_settings: "General settings" + general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods." + activation_and_payment: "Subscription activation and payment" name: "Name" name_max_length: "Name length must be less than 24 characters." group: "Group" transversal: "Transversal plan" transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups." + display: "Display" category: "Category" category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions." number_of_periods: "Number of periods" @@ -173,10 +180,9 @@ en: rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought." monthly_payment: "Monthly payment?" monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months." - description: "Description" information_sheet: "Information sheet" notified_partner: "Notified partner" - new_user: "New user ..." + new_user: "New user" alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user." disabled: "Disable subscription" disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it." @@ -185,9 +191,40 @@ en: partner_plan: "Partner plan" partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan." partner_created: "The partner was successfully created" - ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan" + slots_visibility: "Slots visibility" + slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings." + machines_visibility: "Visibility time limit, in hours (machines)" + visibility_minimum: "Visibility cannot be less than 7 hours" + save: "Save" create_success: "Plan(s) successfully created. Don't forget to redefine prices." update_success: "The plan was updated successfully" + plan_limit_form: + usage_limitation: "Limitation of use" + usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation." + usage_limitation_switch: "Restrict machine reservations to a number of hours per day." + new_usage_limitation: "Add a limitation of use" + all_limitations: "All limitations" + by_category: "By machines category" + by_machine: "By machine" + category: "Machines category" + machine: "Machine name" + max_hours_per_day: "Max. hours/day" + ongoing_limitations: "Ongoing limitations" + saved_limitations: "Saved limitations" + cancel: "Cancel this limitation" + cancel_deletion: "Cancel" + ongoing_deletion: "Ongoing deletion" + plan_limit_modal: + title: "Manage limitation of use" + limit_reservations: "Limit reservations" + by_category: "By machines category" + by_machine: "By machine" + category: "Machines category" + machine: "Machine name" + categories_info: "If you select all machine categories, the limits will apply across the board." + machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten." + max_hours_per_day: "Maximum number of reservation hours per day" + confirm: "Confirm" partner_modal: title: "Create a new partner" create_partner: "Create the partner" @@ -196,6 +233,7 @@ en: email: "Email address" plan_pricing_form: prices: "Prices" + about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour." copy_prices_from: "Copy prices from" copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan" machines: "Machines" @@ -286,7 +324,7 @@ en: manage_trainings: "Click here to add or remove trainings." number_of_tickets: "Number of tickets: " adjust_the_opening_hours: "Adjust the opening hours" - to_time: "to" #eg. from 18:00 to 21:00 + to_time: "to" #e.g. from 18:00 to 21:00 restrict_options: "Restriction options" restrict_with_labels: "Restrict this slot with labels" restrict_for_subscriptions: "Restrict this slot for subscription users" @@ -508,8 +546,8 @@ en: on_DATE: "on {DATE}" from_DATE: "from {DATE}" from_TIME: "from {TIME}" - to_date: "to" #eg: from 01/01 to 01/05 - to_time: "to" #eg. from 18:00 to 21:00 + to_date: "to" #e.g.: from 01/01 to 01/05 + to_time: "to" #e.g. from 18:00 to 21:00 title: "Title" dates: "Dates" booking: "Booking" @@ -885,8 +923,6 @@ en: deleted_user: "Deleted user" refund_invoice_successfully_created: "Refund invoice successfully created." create_a_refund_on_this_invoice: "Create a refund on this invoice" - creation_date_for_the_refund: "Creation date for the refund" - creation_date_is_required: "Creation date is required." refund_mode: "Refund mode:" do_you_want_to_disable_the_user_s_subscription: "Do you want to disabled the user's subscription:" elements_to_refund: "Elements to refund" @@ -1453,8 +1489,8 @@ en: statistics: "Statistics" evolution: "Evolution" age_filter: "Age filter" - from_age: "From" #eg. from 8 to 40 years old - to_age: "to" #eg. from 8 to 40 years old + from_age: "From" #e.g. from 8 to 40 years old + to_age: "to" #e.g. from 8 to 40 years old start: "Start:" end: "End:" custom_filter: "Custom filter" @@ -1572,7 +1608,10 @@ en: visibility_for_other_members: "For all other members" reservation_deadline: "Prevent last minute booking" reservation_deadline_help: "If you increase the prior period, members won't be able to book a slot X minutes before its start." - deadline_minutes: "Prior period (minutes)" + machine_deadline_minutes: "Machine prior period (minutes)" + training_deadline_minutes: "Training prior period (minutes)" + event_deadline_minutes: "Event prior period (minutes)" + space_deadline_minutes: "Space prior period (minutes)" ability_for_the_users_to_move_their_reservations: "Ability for the users to move their reservations" reservations_shifting: "Reservations shifting" prior_period_hours: "Prior period (hours)" @@ -2215,8 +2254,6 @@ en: unexpected_error_occurred: "An unexpected error occurred. Please try again later." all_products: "All products" create_a_product: "Create a product" - successfully_deleted: "The product has been successfully deleted" - unable_to_delete: "Unable to delete the product: " filter: "Filter" filter_clear: "Clear all" filter_apply: "Apply" @@ -2238,6 +2275,7 @@ en: sort: "Sort:" visible_only: "Visible products only" product_item: + product: "product" visible: "visible" hidden: "hidden" stock: @@ -2294,6 +2332,7 @@ en: stocks: "Stock:" internal: "Private stock" external: "Public stock" + edit: "Edit" all: "All types" remaining_stock: "Remaining stock" type_in: "Add" diff --git a/config/locales/app.admin.es.yml b/config/locales/app.admin.es.yml index 3314a1b67..59b37e971 100644 --- a/config/locales/app.admin.es.yml +++ b/config/locales/app.admin.es.yml @@ -2,8 +2,8 @@ es: app: admin: edit_destroy_buttons: - deleted: "The {TYPE} was successfully deleted." - unable_to_delete: "Unable to delete the {TYPE}: " + deleted: "Successfully deleted." + unable_to_delete: "Unable to delete: " delete_item: "Delete the {TYPE}" confirm_delete: "Delete" delete_confirmation: "Are you sure you want to delete this {TYPE}?" @@ -152,12 +152,19 @@ es: every_month: "Every month" every_year: "Every year" plan_form: - general_information: "General information" + ACTION_title: "{ACTION, select, create{New} other{Update the}} plan" + tab_settings: "Settings" + tab_usage_limits: "Usage limits" + description: "Description" + general_settings: "General settings" + general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods." + activation_and_payment: "Subscription activation and payment" name: "Name" name_max_length: "Name length must be less than 24 characters." group: "Group" transversal: "Transversal plan" transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups." + display: "Display" category: "Category" category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions." number_of_periods: "Number of periods" @@ -173,10 +180,9 @@ es: rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought." monthly_payment: "Monthly payment?" monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months." - description: "Description" information_sheet: "Information sheet" notified_partner: "Notified partner" - new_user: "New user ..." + new_user: "New user" alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user." disabled: "Disable subscription" disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it." @@ -185,9 +191,40 @@ es: partner_plan: "Partner plan" partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan." partner_created: "The partner was successfully created" - ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan" + slots_visibility: "Slots visibility" + slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings." + machines_visibility: "Visibility time limit, in hours (machines)" + visibility_minimum: "Visibility cannot be less than 7 hours" + save: "Save" create_success: "Plan(s) successfully created. Don't forget to redefine prices." update_success: "The plan was updated successfully" + plan_limit_form: + usage_limitation: "Limitation of use" + usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation." + usage_limitation_switch: "Restrict machine reservations to a number of hours per day." + new_usage_limitation: "Add a limitation of use" + all_limitations: "All limitations" + by_category: "By machines category" + by_machine: "By machine" + category: "Machines category" + machine: "Machine name" + max_hours_per_day: "Max. hours/day" + ongoing_limitations: "Ongoing limitations" + saved_limitations: "Saved limitations" + cancel: "Cancel this limitation" + cancel_deletion: "Cancel" + ongoing_deletion: "Ongoing deletion" + plan_limit_modal: + title: "Manage limitation of use" + limit_reservations: "Limit reservations" + by_category: "By machines category" + by_machine: "By machine" + category: "Machines category" + machine: "Machine name" + categories_info: "If you select all machine categories, the limits will apply across the board." + machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten." + max_hours_per_day: "Maximum number of reservation hours per day" + confirm: "Confirm" partner_modal: title: "Create a new partner" create_partner: "Create the partner" @@ -196,6 +233,7 @@ es: email: "Email address" plan_pricing_form: prices: "Prices" + about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour." copy_prices_from: "Copy prices from" copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan" machines: "Machines" @@ -286,7 +324,7 @@ es: manage_trainings: "Click here to add or remove trainings." number_of_tickets: "Número de tickets: " adjust_the_opening_hours: "Ajustar el horario de apertura" - to_time: "a" #eg. from 18:00 to 21:00 + to_time: "a" #e.g. from 18:00 to 21:00 restrict_options: "Restriction options" restrict_with_labels: "Restringir este horario con etiquetas" restrict_for_subscriptions: "Restrict this slot for subscription users" @@ -508,8 +546,8 @@ es: on_DATE: "on {DATE}" from_DATE: "Desde {DATE}" from_TIME: "Desde {TIME}" - to_date: "to" #eg: from 01/01 to 01/05 - to_time: "to" #eg. from 18:00 to 21:00 + to_date: "to" #e.g.: from 01/01 to 01/05 + to_time: "to" #e.g. from 18:00 to 21:00 title: "Title" dates: "Dates" booking: "Booking" @@ -885,8 +923,6 @@ es: deleted_user: "Usario eliminado" refund_invoice_successfully_created: "Factura de reembolso creada correctamente." create_a_refund_on_this_invoice: "Crear un reembolso en esta factura" - creation_date_for_the_refund: "Fecha de creación del reembolso" - creation_date_is_required: "Se requiere la fecha de creación." refund_mode: "Modo de reembolso:" do_you_want_to_disable_the_user_s_subscription: "¿Quieres inhabilitar la suscripción del usuario?:" elements_to_refund: "Elementos a reembolsar" @@ -1453,8 +1489,8 @@ es: statistics: "Statistics" evolution: "Evolución" age_filter: "Filtro de edad" - from_age: "Desde" #eg. from 8 to 40 years old - to_age: "a" #eg. from 8 to 40 years old + from_age: "Desde" #e.g. from 8 to 40 years old + to_age: "a" #e.g. from 8 to 40 years old start: "Principio:" end: "Final:" custom_filter: "Filtro personalizado" @@ -1572,7 +1608,10 @@ es: visibility_for_other_members: "Para todos los demás miembros" reservation_deadline: "Prevent last minute booking" reservation_deadline_help: "If you increase the prior period, members won't be able to book a slot X minutes before its start." - deadline_minutes: "Prior period (minutes)" + machine_deadline_minutes: "Machine prior period (minutes)" + training_deadline_minutes: "Training prior period (minutes)" + event_deadline_minutes: "Event prior period (minutes)" + space_deadline_minutes: "Space prior period (minutes)" ability_for_the_users_to_move_their_reservations: "Capacidad para que los usuarios muevan sus reservas" reservations_shifting: "Cambio de reservas" prior_period_hours: "Período anterior (horas)" @@ -2215,8 +2254,6 @@ es: unexpected_error_occurred: "An unexpected error occurred. Please try again later." all_products: "All products" create_a_product: "Create a product" - successfully_deleted: "The product has been successfully deleted" - unable_to_delete: "Unable to delete the product: " filter: "Filter" filter_clear: "Clear all" filter_apply: "Apply" @@ -2238,6 +2275,7 @@ es: sort: "Sort:" visible_only: "Visible products only" product_item: + product: "product" visible: "visible" hidden: "hidden" stock: @@ -2294,6 +2332,7 @@ es: stocks: "Stock:" internal: "Private stock" external: "Public stock" + edit: "Edit" all: "All types" remaining_stock: "Remaining stock" type_in: "Add" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 7fbe35394..e7082ef58 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -152,12 +152,19 @@ fr: every_month: "Chaque mois" every_year: "Chaque année" plan_form: - general_information: "Informations générales" + ACTION_title: "{ACTION, select, create{Nouvelle} other{Mettre à jour la}} formule d'abonnement" + tab_settings: "Paramètres" + tab_usage_limits: "Limites d'utilisation" + description: "Description" + general_settings: "Paramètres généraux" + general_settings_info: "Déterminez à quel groupe cet abonnement est dédié. Définissez également son prix et sa durée en périodes." + activation_and_payment: "Activation et paiement de l'abonnement" name: "Nom" name_max_length: "Le nom doit faire moins de 24 caractères." group: "Groupe" transversal: "Abonnement transversal" transversal_help: "Si cette option est cochée, une copie de cette formule d'abonnement sera créée pour chaque groupe actuellement activé." + display: "Affichage" category: "Catégorie" category_help: "Les catégories vous permettent de regrouper les formules d'abonnement, sur la vue publique des abonnements." number_of_periods: "Nombre de périodes" @@ -173,7 +180,6 @@ fr: rolling_subscription_help: "Un abonnement glissant commencera le jour de la première formation. Sinon, il commencera dès qu'il est acheté." monthly_payment: "Paiement mensuel ?" monthly_payment_help: "Si le paiement mensuel est activé, les membres pourront choisir entre un paiement unique ou un échéancier de paiement échelonné chaque mois." - description: "Description" information_sheet: "Fiche descriptive" notified_partner: "Partenaire notifié" new_user: "Nouvel utilisateur ..." @@ -185,9 +191,40 @@ fr: partner_plan: "Abonnement partenaire" partner_plan_help: "Vous pouvez vendre des abonnements en partenariat avec un autre organisme. Ce faisant, l'autre entité sera informée lorsqu'un membre s'abonne à cette formule d'abonnement." partner_created: "Le partenaire a bien été créé" - ACTION_plan: "{ACTION, select, create{Créer} other{Mettre à jour}} la formule d'abonnement" + slots_visibility: "Visibilité des créneaux" + slots_visibility_help: "Vous pouvez déterminer combien de temps en avance les abonnés peuvent voir et réserver les créneaux machines. Lorsque ce paramètre est défini, il devient prioritaire sur les paramètres généraux." + machines_visibility: "Délai de visibilité, en heures (machines)" + visibility_minimum: "La visibilité ne peut pas être inférieure à 7 heures" + save: "Enregistrer" create_success: "Création du/des formule(s) d'abonnement réussie(s). N'oubliez pas de redéfinir les tarifs." update_success: "La formule d'abonnement a bien été mise à jour" + plan_limit_form: + usage_limitation: "Limite d'usage" + usage_limitation_info: "Définissez un nombre maximum d'heures de réservation par jour et par catégorie de machine. Les catégories de machines qui n'ont aucun paramètre configuré ne seront soumises à aucune limitation." + usage_limitation_switch: "Restreindre les réservations de machines à un certain nombre d'heures par jour." + new_usage_limitation: "Ajouter une limite d'usage" + all_limitations: "Toutes les limitations" + by_category: "Par catégorie de machines" + by_machine: "Par machine" + category: "Catégorie de machines" + machine: "Nom machine" + max_hours_per_day: "Max. heures/jour" + ongoing_limitations: "Limites en cours" + saved_limitations: "Limites enregistrées" + cancel: "Annuler cette limite" + cancel_deletion: "Annuler" + ongoing_deletion: "Suppression en cours" + plan_limit_modal: + title: "Gérer la limite d'usage" + limit_reservations: "Limiter les réservations" + by_category: "Par catégorie de machines" + by_machine: "Par machine" + category: "Catégorie de machines" + machine: "Nom machine" + categories_info: "Si vous sélectionnez toutes les catégories de machines, les limites s'appliqueront à tous les niveaux." + machine_info: "Veuillez noter que si vous avez déjà créé une limite pour la catégorie de machines incluant la machine sélectionnée, elle sera définitivement écrasée." + max_hours_per_day: "Nombre maximum d'heures de réservation par jour" + confirm: "Confirmer" partner_modal: title: "Créer un nouveau partenaire" create_partner: "Créer le partenaire" @@ -196,6 +233,7 @@ fr: email: "Adresse électronique" plan_pricing_form: prices: "Tarifs" + about_prices: "Les prix définis ici s'appliqueront aux membres qui s'abonneront à cette formule d'abonnement, pour les machines et les espaces." copy_prices_from: "Copier les prix depuis" copy_prices_from_help: "Cela remplacera tous les prix de cette formule d'abonnement par les prix de la formule sélectionnée" machines: "Machines" @@ -286,7 +324,7 @@ fr: manage_trainings: "Cliquez-ici pour ajouter ou supprimer des formations." number_of_tickets: "Nombre de places : " adjust_the_opening_hours: "Ajuster l'horaire" - to_time: "à" #eg. from 18:00 to 21:00 + to_time: "à" #e.g. from 18:00 to 21:00 restrict_options: "Options de restriction" restrict_with_labels: "Restreindre ce créneau avec des étiquettes" restrict_for_subscriptions: "Restreindre ce créneau pour les abonnements" @@ -508,8 +546,8 @@ fr: on_DATE: "le {DATE}" from_DATE: "du {DATE}" from_TIME: "de {TIME}" - to_date: "au" #eg: from 01/01 to 01/05 - to_time: "à" #eg. from 18:00 to 21:00 + to_date: "au" #e.g.: from 01/01 to 01/05 + to_time: "à" #e.g. from 18:00 to 21:00 title: "Titre" dates: "Dates" booking: "Réservations" @@ -885,8 +923,6 @@ fr: deleted_user: "Utilisateur supprimé" refund_invoice_successfully_created: "La facture d'avoir a bien été créée." create_a_refund_on_this_invoice: "Générer un avoir sur cette facture" - creation_date_for_the_refund: "Date d'émission de l'avoir" - creation_date_is_required: "La date d'émission est requise." refund_mode: "Mode de remboursement :" do_you_want_to_disable_the_user_s_subscription: "Souhaitez-vous désactiver l'abonnement de l'utilisateur :" elements_to_refund: "Éléments à rembourser" @@ -1453,8 +1489,8 @@ fr: statistics: "Statistiques" evolution: "Évolution" age_filter: "Filtre d'âge" - from_age: "De" #eg. from 8 to 40 years old - to_age: "à" #eg. from 8 to 40 years old + from_age: "De" #e.g. from 8 to 40 years old + to_age: "à" #e.g. from 8 to 40 years old start: "Début :" end: "Fin :" custom_filter: "Filtre personnalisé" @@ -1572,7 +1608,10 @@ fr: visibility_for_other_members: "Pour tous les autres membres" reservation_deadline: "Empêcher les réservations de dernière minute" reservation_deadline_help: "Si vous augmentez le délai préalable, les membres ne pourront pas réserver un créneau X minutes avant le début de celui-ci." - deadline_minutes: "Délai préalable (en minutes)" + machine_deadline_minutes: "Délai préalable pour les machines (en minutes)" + training_deadline_minutes: "Délai préalable pour les formations (en minutes)" + event_deadline_minutes: "Délai préalable pour les événements (en minutes)" + space_deadline_minutes: "Délai préalable pour les espaces (en minutes)" ability_for_the_users_to_move_their_reservations: "Possibilité pour l'utilisateur de déplacer ses réservations" reservations_shifting: "Déplacement des réservations" prior_period_hours: "Délai préalable (en heures)" @@ -2215,8 +2254,6 @@ fr: unexpected_error_occurred: "Une erreur inattendue s'est produite. Veuillez réessayer ultérieurement." all_products: "Tous les produits" create_a_product: "Créer un produit" - successfully_deleted: "Le produit a bien été supprimé" - unable_to_delete: "Impossible de supprimer le produit : " filter: "Filter" filter_clear: "Tout effacer" filter_apply: "Appliquer" @@ -2238,6 +2275,7 @@ fr: sort: "Trier :" visible_only: "Produits visibles uniquement" product_item: + product: "produit" visible: "visible" hidden: "caché" stock: @@ -2294,6 +2332,7 @@ fr: stocks: "Stock :" internal: "Stock interne" external: "Stock externe" + edit: "Modifier" all: "Tous types" remaining_stock: "Stock restant" type_in: "Ajouter" diff --git a/config/locales/app.admin.no.yml b/config/locales/app.admin.no.yml index 5526bf3e5..352d90f9d 100644 --- a/config/locales/app.admin.no.yml +++ b/config/locales/app.admin.no.yml @@ -2,8 +2,8 @@ app: admin: edit_destroy_buttons: - deleted: "The {TYPE} was successfully deleted." - unable_to_delete: "Unable to delete the {TYPE}: " + deleted: "Successfully deleted." + unable_to_delete: "Unable to delete: " delete_item: "Delete the {TYPE}" confirm_delete: "Delete" delete_confirmation: "Are you sure you want to delete this {TYPE}?" @@ -152,12 +152,19 @@ every_month: "Every month" every_year: "Every year" plan_form: - general_information: "General information" + ACTION_title: "{ACTION, select, create{New} other{Update the}} plan" + tab_settings: "Settings" + tab_usage_limits: "Usage limits" + description: "Description" + general_settings: "General settings" + general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods." + activation_and_payment: "Subscription activation and payment" name: "Name" name_max_length: "Name length must be less than 24 characters." group: "Group" transversal: "Transversal plan" transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups." + display: "Display" category: "Category" category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions." number_of_periods: "Number of periods" @@ -173,10 +180,9 @@ rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought." monthly_payment: "Monthly payment?" monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months." - description: "Description" information_sheet: "Information sheet" notified_partner: "Notified partner" - new_user: "New user ..." + new_user: "New user" alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user." disabled: "Disable subscription" disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it." @@ -185,9 +191,40 @@ partner_plan: "Partner plan" partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan." partner_created: "The partner was successfully created" - ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan" + slots_visibility: "Slots visibility" + slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings." + machines_visibility: "Visibility time limit, in hours (machines)" + visibility_minimum: "Visibility cannot be less than 7 hours" + save: "Save" create_success: "Plan(s) successfully created. Don't forget to redefine prices." update_success: "The plan was updated successfully" + plan_limit_form: + usage_limitation: "Limitation of use" + usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation." + usage_limitation_switch: "Restrict machine reservations to a number of hours per day." + new_usage_limitation: "Add a limitation of use" + all_limitations: "All limitations" + by_category: "By machines category" + by_machine: "By machine" + category: "Machines category" + machine: "Machine name" + max_hours_per_day: "Max. hours/day" + ongoing_limitations: "Ongoing limitations" + saved_limitations: "Saved limitations" + cancel: "Cancel this limitation" + cancel_deletion: "Cancel" + ongoing_deletion: "Ongoing deletion" + plan_limit_modal: + title: "Manage limitation of use" + limit_reservations: "Limit reservations" + by_category: "By machines category" + by_machine: "By machine" + category: "Machines category" + machine: "Machine name" + categories_info: "If you select all machine categories, the limits will apply across the board." + machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten." + max_hours_per_day: "Maximum number of reservation hours per day" + confirm: "Confirm" partner_modal: title: "Create a new partner" create_partner: "Create the partner" @@ -196,6 +233,7 @@ email: "Email address" plan_pricing_form: prices: "Prices" + about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour." copy_prices_from: "Copy prices from" copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan" machines: "Machines" @@ -286,7 +324,7 @@ manage_trainings: "Klikk her for å legge til eller endre opplæring." number_of_tickets: "Antall billetter: " adjust_the_opening_hours: "Endre åpningstid" - to_time: "til" #eg. from 18:00 to 21:00 + to_time: "til" #e.g. from 18:00 to 21:00 restrict_options: "Alternativer for begrensning" restrict_with_labels: "Begrens denne reservasjonen med etiketter" restrict_for_subscriptions: "Begrens denne reservasjoen til medlemmer" @@ -508,8 +546,8 @@ on_DATE: "{DATE}" from_DATE: "fra {DATE}" from_TIME: "fra {TIME}" - to_date: "til" #eg: from 01/01 to 01/05 - to_time: "til" #eg. from 18:00 to 21:00 + to_date: "til" #e.g.: from 01/01 to 01/05 + to_time: "til" #e.g. from 18:00 to 21:00 title: "Tittel" dates: "Datoer" booking: "Reservasjon" @@ -885,8 +923,6 @@ deleted_user: "Slettet bruker" refund_invoice_successfully_created: "Refusjon ble opprettet." create_a_refund_on_this_invoice: "Opprett en refusjon på denne fakturaen" - creation_date_for_the_refund: "Refusjonsdato" - creation_date_is_required: "Opprettelsesdato er påkrevd." refund_mode: "Refusjonsmodus:" do_you_want_to_disable_the_user_s_subscription: "Ønsker du å deaktivere brukerens abonnement/medlemskap:" elements_to_refund: "Elementer for tilbakebetaling" @@ -1453,8 +1489,8 @@ statistics: "Statistikk" evolution: "Utvikling" age_filter: "Aldersfilter" - from_age: "Fra" #eg. from 8 to 40 years old - to_age: "til" #eg. from 8 to 40 years old + from_age: "Fra" #e.g. from 8 to 40 years old + to_age: "til" #e.g. from 8 to 40 years old start: "Start:" end: "Slutt:" custom_filter: "Egendefinerte filtre" @@ -1572,7 +1608,10 @@ visibility_for_other_members: "For alle andre medlemmer" reservation_deadline: "Prevent last minute booking" reservation_deadline_help: "If you increase the prior period, members won't be able to book a slot X minutes before its start." - deadline_minutes: "Prior period (minutes)" + machine_deadline_minutes: "Machine prior period (minutes)" + training_deadline_minutes: "Training prior period (minutes)" + event_deadline_minutes: "Event prior period (minutes)" + space_deadline_minutes: "Space prior period (minutes)" ability_for_the_users_to_move_their_reservations: "Om brukerne kan flytte sine bestillinger" reservations_shifting: "Reservasjonsendringer" prior_period_hours: "Tidligere perioder (timer)" @@ -2215,8 +2254,6 @@ unexpected_error_occurred: "An unexpected error occurred. Please try again later." all_products: "All products" create_a_product: "Create a product" - successfully_deleted: "The product has been successfully deleted" - unable_to_delete: "Unable to delete the product: " filter: "Filter" filter_clear: "Clear all" filter_apply: "Apply" @@ -2238,6 +2275,7 @@ sort: "Sort:" visible_only: "Visible products only" product_item: + product: "product" visible: "visible" hidden: "hidden" stock: @@ -2294,6 +2332,7 @@ stocks: "Stock:" internal: "Private stock" external: "Public stock" + edit: "Edit" all: "All types" remaining_stock: "Remaining stock" type_in: "Add" diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index eb5517437..3af29bcbb 100644 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -2,8 +2,8 @@ pt: app: admin: edit_destroy_buttons: - deleted: "The {TYPE} was successfully deleted." - unable_to_delete: "Unable to delete the {TYPE}: " + deleted: "Successfully deleted." + unable_to_delete: "Unable to delete: " delete_item: "Delete the {TYPE}" confirm_delete: "Delete" delete_confirmation: "Are you sure you want to delete this {TYPE}?" @@ -152,12 +152,19 @@ pt: every_month: "Every month" every_year: "Every year" plan_form: - general_information: "General information" + ACTION_title: "{ACTION, select, create{New} other{Update the}} plan" + tab_settings: "Settings" + tab_usage_limits: "Usage limits" + description: "Description" + general_settings: "General settings" + general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods." + activation_and_payment: "Subscription activation and payment" name: "Name" name_max_length: "Name length must be less than 24 characters." group: "Group" transversal: "Transversal plan" transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups." + display: "Display" category: "Category" category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions." number_of_periods: "Number of periods" @@ -173,10 +180,9 @@ pt: rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought." monthly_payment: "Monthly payment?" monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months." - description: "Description" information_sheet: "Information sheet" notified_partner: "Notified partner" - new_user: "New user ..." + new_user: "New user" alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user." disabled: "Disable subscription" disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it." @@ -185,9 +191,40 @@ pt: partner_plan: "Partner plan" partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan." partner_created: "The partner was successfully created" - ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan" + slots_visibility: "Slots visibility" + slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings." + machines_visibility: "Visibility time limit, in hours (machines)" + visibility_minimum: "Visibility cannot be less than 7 hours" + save: "Save" create_success: "Plan(s) successfully created. Don't forget to redefine prices." update_success: "The plan was updated successfully" + plan_limit_form: + usage_limitation: "Limitation of use" + usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation." + usage_limitation_switch: "Restrict machine reservations to a number of hours per day." + new_usage_limitation: "Add a limitation of use" + all_limitations: "All limitations" + by_category: "By machines category" + by_machine: "By machine" + category: "Machines category" + machine: "Machine name" + max_hours_per_day: "Max. hours/day" + ongoing_limitations: "Ongoing limitations" + saved_limitations: "Saved limitations" + cancel: "Cancel this limitation" + cancel_deletion: "Cancel" + ongoing_deletion: "Ongoing deletion" + plan_limit_modal: + title: "Manage limitation of use" + limit_reservations: "Limit reservations" + by_category: "By machines category" + by_machine: "By machine" + category: "Machines category" + machine: "Machine name" + categories_info: "If you select all machine categories, the limits will apply across the board." + machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten." + max_hours_per_day: "Maximum number of reservation hours per day" + confirm: "Confirm" partner_modal: title: "Create a new partner" create_partner: "Create the partner" @@ -196,6 +233,7 @@ pt: email: "Email address" plan_pricing_form: prices: "Prices" + about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour." copy_prices_from: "Copy prices from" copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan" machines: "Machines" @@ -286,7 +324,7 @@ pt: manage_trainings: "Clique aqui para adicionar ou remover treinamentos." number_of_tickets: "Número de vagas: " adjust_the_opening_hours: "Ajustar o horário de funcionamento" - to_time: "ás" #eg. from 18:00 to 21:00 + to_time: "ás" #e.g. from 18:00 to 21:00 restrict_options: "Opções de restrição" restrict_with_labels: "Restrinja este slot com etiquetas" restrict_for_subscriptions: "Restringir este slot para os usuários da assinatura" @@ -508,8 +546,8 @@ pt: on_DATE: "No {DATE}" from_DATE: "Em {DATE}" from_TIME: "Ás {TIME}" - to_date: "ás" #eg: from 01/01 to 01/05 - to_time: "ás" #eg. from 18:00 to 21:00 + to_date: "ás" #e.g.: from 01/01 to 01/05 + to_time: "ás" #e.g. from 18:00 to 21:00 title: "Título" dates: "Datas" booking: "Reserva" @@ -885,8 +923,6 @@ pt: deleted_user: "Usuário deletado" refund_invoice_successfully_created: "Restituição de fatura criada com sucesso." create_a_refund_on_this_invoice: "Criar restituição de fatura" - creation_date_for_the_refund: "Criação de data de restituição" - creation_date_is_required: "Data de criação é obrigatório." refund_mode: "Modo de restituição:" do_you_want_to_disable_the_user_s_subscription: "Você deseja desativar a inscrição de usuários:" elements_to_refund: "Elementos para restituição" @@ -1453,8 +1489,8 @@ pt: statistics: "Estatísticas" evolution: "Evolução" age_filter: "Filtro de idade" - from_age: "Dos" #eg. from 8 to 40 years old - to_age: "aos" #eg. from 8 to 40 years old + from_age: "Dos" #e.g. from 8 to 40 years old + to_age: "aos" #e.g. from 8 to 40 years old start: "Início:" end: "Fim:" custom_filter: "Filtro customizado" @@ -1572,7 +1608,10 @@ pt: visibility_for_other_members: "Para todos os outros membros" reservation_deadline: "Impedir a reserva da última hora" reservation_deadline_help: "Se você aumentar o período prévio, os membros não serão capazes de reservar um slot X minutos antes do seu início." - deadline_minutes: "Período prévio (minutos)" + machine_deadline_minutes: "Machine prior period (minutes)" + training_deadline_minutes: "Training prior period (minutes)" + event_deadline_minutes: "Event prior period (minutes)" + space_deadline_minutes: "Space prior period (minutes)" ability_for_the_users_to_move_their_reservations: "Habilidade para os usuários mover suas reservas" reservations_shifting: "Mudança de reservas" prior_period_hours: "Período anterior (horas)" @@ -2215,8 +2254,6 @@ pt: unexpected_error_occurred: "An unexpected error occurred. Please try again later." all_products: "All products" create_a_product: "Create a product" - successfully_deleted: "The product has been successfully deleted" - unable_to_delete: "Unable to delete the product: " filter: "Filter" filter_clear: "Clear all" filter_apply: "Apply" @@ -2238,6 +2275,7 @@ pt: sort: "Sort:" visible_only: "Visible products only" product_item: + product: "product" visible: "visible" hidden: "hidden" stock: @@ -2294,6 +2332,7 @@ pt: stocks: "Stock:" internal: "Private stock" external: "Public stock" + edit: "Edit" all: "All types" remaining_stock: "Remaining stock" type_in: "Add" diff --git a/config/locales/app.admin.zu.yml b/config/locales/app.admin.zu.yml index bb75b5f93..65698c181 100644 --- a/config/locales/app.admin.zu.yml +++ b/config/locales/app.admin.zu.yml @@ -2,8 +2,8 @@ zu: app: admin: edit_destroy_buttons: - deleted: "crwdns36793:0{TYPE}crwdne36793:0" - unable_to_delete: "crwdns36795:0{TYPE}crwdne36795:0" + deleted: "crwdns36793:0crwdne36793:0" + unable_to_delete: "crwdns36795:0crwdne36795:0" delete_item: "crwdns36797:0{TYPE}crwdne36797:0" confirm_delete: "crwdns36799:0crwdne36799:0" delete_confirmation: "crwdns36801:0{TYPE}crwdne36801:0" @@ -152,12 +152,19 @@ zu: every_month: "crwdns31885:0crwdne31885:0" every_year: "crwdns31887:0crwdne31887:0" plan_form: - general_information: "crwdns31889:0crwdne31889:0" + ACTION_title: "crwdns37397:0ACTION={ACTION}crwdne37397:0" + tab_settings: "crwdns37399:0crwdne37399:0" + tab_usage_limits: "crwdns37401:0crwdne37401:0" + description: "crwdns31931:0crwdne31931:0" + general_settings: "crwdns37403:0crwdne37403:0" + general_settings_info: "crwdns37405:0crwdne37405:0" + activation_and_payment: "crwdns37407:0crwdne37407:0" name: "crwdns31891:0crwdne31891:0" name_max_length: "crwdns31893:0crwdne31893:0" group: "crwdns31895:0crwdne31895:0" transversal: "crwdns31897:0crwdne31897:0" transversal_help: "crwdns31899:0crwdne31899:0" + display: "crwdns37409:0crwdne37409:0" category: "crwdns31901:0crwdne31901:0" category_help: "crwdns31903:0crwdne31903:0" number_of_periods: "crwdns31905:0crwdne31905:0" @@ -173,7 +180,6 @@ zu: rolling_subscription_help: "crwdns31925:0crwdne31925:0" monthly_payment: "crwdns31927:0crwdne31927:0" monthly_payment_help: "crwdns31929:0crwdne31929:0" - description: "crwdns31931:0crwdne31931:0" information_sheet: "crwdns31933:0crwdne31933:0" notified_partner: "crwdns31935:0crwdne31935:0" new_user: "crwdns31937:0crwdne31937:0" @@ -185,9 +191,40 @@ zu: partner_plan: "crwdns31949:0crwdne31949:0" partner_plan_help: "crwdns31951:0crwdne31951:0" partner_created: "crwdns31953:0crwdne31953:0" - ACTION_plan: "crwdns31955:0ACTION={ACTION}crwdne31955:0" + slots_visibility: "crwdns37491:0crwdne37491:0" + slots_visibility_help: "crwdns37493:0crwdne37493:0" + machines_visibility: "crwdns37495:0crwdne37495:0" + visibility_minimum: "crwdns37589:0crwdne37589:0" + save: "crwdns37411:0crwdne37411:0" create_success: "crwdns31957:0crwdne31957:0" update_success: "crwdns31959:0crwdne31959:0" + plan_limit_form: + usage_limitation: "crwdns37413:0crwdne37413:0" + usage_limitation_info: "crwdns37415:0crwdne37415:0" + usage_limitation_switch: "crwdns37417:0crwdne37417:0" + new_usage_limitation: "crwdns37419:0crwdne37419:0" + all_limitations: "crwdns37421:0crwdne37421:0" + by_category: "crwdns37477:0crwdne37477:0" + by_machine: "crwdns37425:0crwdne37425:0" + category: "crwdns37427:0crwdne37427:0" + machine: "crwdns37429:0crwdne37429:0" + max_hours_per_day: "crwdns37431:0crwdne37431:0" + ongoing_limitations: "crwdns37433:0crwdne37433:0" + saved_limitations: "crwdns37435:0crwdne37435:0" + cancel: "crwdns37437:0crwdne37437:0" + cancel_deletion: "crwdns37487:0crwdne37487:0" + ongoing_deletion: "crwdns37489:0crwdne37489:0" + plan_limit_modal: + title: "crwdns37445:0crwdne37445:0" + limit_reservations: "crwdns37447:0crwdne37447:0" + by_category: "crwdns37479:0crwdne37479:0" + by_machine: "crwdns37451:0crwdne37451:0" + category: "crwdns37453:0crwdne37453:0" + machine: "crwdns37455:0crwdne37455:0" + categories_info: "crwdns37457:0crwdne37457:0" + machine_info: "crwdns37459:0crwdne37459:0" + max_hours_per_day: "crwdns37461:0crwdne37461:0" + confirm: "crwdns37463:0crwdne37463:0" partner_modal: title: "crwdns31961:0crwdne31961:0" create_partner: "crwdns31963:0crwdne31963:0" @@ -196,6 +233,7 @@ zu: email: "crwdns31969:0crwdne31969:0" plan_pricing_form: prices: "crwdns31971:0crwdne31971:0" + about_prices: "crwdns37465:0crwdne37465:0" copy_prices_from: "crwdns31973:0crwdne31973:0" copy_prices_from_help: "crwdns31975:0crwdne31975:0" machines: "crwdns31977:0crwdne31977:0" @@ -286,7 +324,7 @@ zu: manage_trainings: "crwdns24132:0crwdne24132:0" number_of_tickets: "crwdns24134:0crwdne24134:0" adjust_the_opening_hours: "crwdns24136:0crwdne24136:0" - to_time: "crwdns24138:0crwdne24138:0" #eg. from 18:00 to 21:00 + to_time: "crwdns24138:0crwdne24138:0" #e.g. from 18:00 to 21:00 restrict_options: "crwdns24140:0crwdne24140:0" restrict_with_labels: "crwdns24142:0crwdne24142:0" restrict_for_subscriptions: "crwdns24144:0crwdne24144:0" @@ -508,8 +546,8 @@ zu: on_DATE: "crwdns24452:0{DATE}crwdne24452:0" from_DATE: "crwdns24454:0{DATE}crwdne24454:0" from_TIME: "crwdns24456:0{TIME}crwdne24456:0" - to_date: "crwdns24458:0crwdne24458:0" #eg: from 01/01 to 01/05 - to_time: "crwdns24460:0crwdne24460:0" #eg. from 18:00 to 21:00 + to_date: "crwdns24458:0crwdne24458:0" #e.g.: from 01/01 to 01/05 + to_time: "crwdns24460:0crwdne24460:0" #e.g. from 18:00 to 21:00 title: "crwdns24462:0crwdne24462:0" dates: "crwdns24464:0crwdne24464:0" booking: "crwdns24466:0crwdne24466:0" @@ -885,8 +923,6 @@ zu: deleted_user: "crwdns25138:0crwdne25138:0" refund_invoice_successfully_created: "crwdns25140:0crwdne25140:0" create_a_refund_on_this_invoice: "crwdns25142:0crwdne25142:0" - creation_date_for_the_refund: "crwdns25144:0crwdne25144:0" - creation_date_is_required: "crwdns25146:0crwdne25146:0" refund_mode: "crwdns25148:0crwdne25148:0" do_you_want_to_disable_the_user_s_subscription: "crwdns25150:0crwdne25150:0" elements_to_refund: "crwdns25152:0crwdne25152:0" @@ -1453,8 +1489,8 @@ zu: statistics: "crwdns26224:0crwdne26224:0" evolution: "crwdns26226:0crwdne26226:0" age_filter: "crwdns26228:0crwdne26228:0" - from_age: "crwdns26230:0crwdne26230:0" #eg. from 8 to 40 years old - to_age: "crwdns26232:0crwdne26232:0" #eg. from 8 to 40 years old + from_age: "crwdns26230:0crwdne26230:0" #e.g. from 8 to 40 years old + to_age: "crwdns26232:0crwdne26232:0" #e.g. from 8 to 40 years old start: "crwdns26234:0crwdne26234:0" end: "crwdns26236:0crwdne26236:0" custom_filter: "crwdns26238:0crwdne26238:0" @@ -1572,7 +1608,10 @@ zu: visibility_for_other_members: "crwdns26448:0crwdne26448:0" reservation_deadline: "crwdns31751:0crwdne31751:0" reservation_deadline_help: "crwdns36265:0crwdne36265:0" - deadline_minutes: "crwdns31753:0crwdne31753:0" + machine_deadline_minutes: "crwdns37593:0crwdne37593:0" + training_deadline_minutes: "crwdns37595:0crwdne37595:0" + event_deadline_minutes: "crwdns37597:0crwdne37597:0" + space_deadline_minutes: "crwdns37599:0crwdne37599:0" ability_for_the_users_to_move_their_reservations: "crwdns26450:0crwdne26450:0" reservations_shifting: "crwdns26452:0crwdne26452:0" prior_period_hours: "crwdns26454:0crwdne26454:0" @@ -2215,8 +2254,6 @@ zu: unexpected_error_occurred: "crwdns31338:0crwdne31338:0" all_products: "crwdns31340:0crwdne31340:0" create_a_product: "crwdns31342:0crwdne31342:0" - successfully_deleted: "crwdns31344:0crwdne31344:0" - unable_to_delete: "crwdns31346:0crwdne31346:0" filter: "crwdns31348:0crwdne31348:0" filter_clear: "crwdns31350:0crwdne31350:0" filter_apply: "crwdns31352:0crwdne31352:0" @@ -2238,6 +2275,7 @@ zu: sort: "crwdns31380:0crwdne31380:0" visible_only: "crwdns31382:0crwdne31382:0" product_item: + product: "crwdns37467:0crwdne37467:0" visible: "crwdns31384:0crwdne31384:0" hidden: "crwdns31386:0crwdne31386:0" stock: @@ -2294,6 +2332,7 @@ zu: stocks: "crwdns31480:0crwdne31480:0" internal: "crwdns31482:0crwdne31482:0" external: "crwdns31484:0crwdne31484:0" + edit: "crwdns37469:0crwdne37469:0" all: "crwdns31486:0crwdne31486:0" remaining_stock: "crwdns31488:0crwdne31488:0" type_in: "crwdns31490:0crwdne31490:0" diff --git a/config/locales/app.shared.de.yml b/config/locales/app.shared.de.yml index a144ffe52..74fc218a7 100644 --- a/config/locales/app.shared.de.yml +++ b/config/locales/app.shared.de.yml @@ -103,6 +103,7 @@ de: must_accept_terms: "You must accept the terms and conditions" save: "Save" gender_input: + label: "Gender" man: "Man" woman: "Woman" change_password: @@ -233,8 +234,6 @@ de: credit_label: 'Legen Sie den Betrag der Gutschrift fest' confirm_credit_label: 'Bestätigen Sie den Betrag der Gutschrift' generate_a_refund_invoice: "Erstelle eine Rückerstattungs-Rechnung" - creation_date_for_the_refund: "Erstellungsdatum für die Erstattung" - creation_date_is_required: "Erstellungsdatum ist erforderlich." description_optional: "Beschreibung (optional):" will_appear_on_the_refund_invoice: "Wird auf der Rückerstattungsrechnung angezeigt." to_credit: 'Guthaben' @@ -540,3 +539,6 @@ de: show_reserved_uniq: "Show only slots with reservations" machine: machine_uncategorized: "Uncategorized machines" + form_unsaved_list: + save_reminder: "Do not forget to save your changes" + cancel: "Cancel" diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 18a3da175..81a88c118 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -103,6 +103,7 @@ en: must_accept_terms: "You must accept the terms and conditions" save: "Save" gender_input: + label: "Gender" man: "Man" woman: "Woman" change_password: @@ -233,8 +234,6 @@ en: credit_label: 'Set the amount to be credited' confirm_credit_label: 'Confirm the amount to be credited' generate_a_refund_invoice: "Generate a refund invoice" - creation_date_for_the_refund: "Creation date for the refund" - creation_date_is_required: "Creation date is required." description_optional: "Description (optional):" will_appear_on_the_refund_invoice: "Will appear on the refund invoice." to_credit: 'Credit' @@ -540,3 +539,6 @@ en: show_reserved_uniq: "Show only slots with reservations" machine: machine_uncategorized: "Uncategorized machines" + form_unsaved_list: + save_reminder: "Do not forget to save your changes" + cancel: "Cancel" diff --git a/config/locales/app.shared.es.yml b/config/locales/app.shared.es.yml index 163ac6409..1b068af65 100644 --- a/config/locales/app.shared.es.yml +++ b/config/locales/app.shared.es.yml @@ -103,6 +103,7 @@ es: must_accept_terms: "You must accept the terms and conditions" save: "Save" gender_input: + label: "Gender" man: "Man" woman: "Woman" change_password: @@ -233,8 +234,6 @@ es: credit_label: 'Selecciona la cantidad a creditar' confirm_credit_label: 'Confirma la cantidad a creditar' generate_a_refund_invoice: "Generar informe de devolución" - creation_date_for_the_refund: "Fecha de creación del informe de devolución" - creation_date_is_required: "Se requiere fecha de creación." description_optional: "Descripción (opcional):" will_appear_on_the_refund_invoice: "Aparecerá en el informe de devolución." to_credit: 'Credito' @@ -540,3 +539,6 @@ es: show_reserved_uniq: "Show only slots with reservations" machine: machine_uncategorized: "Uncategorized machines" + form_unsaved_list: + save_reminder: "Do not forget to save your changes" + cancel: "Cancel" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index 014695efe..c5da79a36 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -103,6 +103,7 @@ fr: must_accept_terms: "Vous devez accepter les conditions générales" save: "Enregistrer" gender_input: + label: "Genre" man: "Homme" woman: "Femme" change_password: @@ -233,8 +234,6 @@ fr: credit_label: 'Indiquez le montant à créditer' confirm_credit_label: 'Confirmez le montant à créditer' generate_a_refund_invoice: "Générer une facture d'avoir" - creation_date_for_the_refund: "Date d'émission de l'avoir" - creation_date_is_required: "La date d'émission est requise." description_optional: "Description (optionnelle) :" will_appear_on_the_refund_invoice: "Apparaîtra sur la facture de remboursement." to_credit: 'Créditer' @@ -540,3 +539,6 @@ fr: show_reserved_uniq: "Afficher uniquement les créneaux avec réservation" machine: machine_uncategorized: "Machines non classés" + form_unsaved_list: + save_reminder: "N'oubliez pas d'enregistrer vos modifications" + cancel: "Annuler" diff --git a/config/locales/app.shared.no.yml b/config/locales/app.shared.no.yml index 37432f921..7a9cb4b0c 100644 --- a/config/locales/app.shared.no.yml +++ b/config/locales/app.shared.no.yml @@ -103,6 +103,7 @@ must_accept_terms: "You must accept the terms and conditions" save: "Save" gender_input: + label: "Gender" man: "Man" woman: "Woman" change_password: @@ -233,8 +234,6 @@ credit_label: 'Velg beløp for kreditering' confirm_credit_label: 'Bekreft beløpet som skal krediteres' generate_a_refund_invoice: "Genererer en refusjons- faktura" - creation_date_for_the_refund: "Refusjonsdato" - creation_date_is_required: "Opprettelsesdato er påkrevd." description_optional: "Beskrivelse (valgfritt):" will_appear_on_the_refund_invoice: "Vises på refusjonsfakturaen." to_credit: 'Kreditt' @@ -540,3 +539,6 @@ show_reserved_uniq: "Show only slots with reservations" machine: machine_uncategorized: "Uncategorized machines" + form_unsaved_list: + save_reminder: "Do not forget to save your changes" + cancel: "Cancel" diff --git a/config/locales/app.shared.pt.yml b/config/locales/app.shared.pt.yml index d10597589..b83bcad2c 100644 --- a/config/locales/app.shared.pt.yml +++ b/config/locales/app.shared.pt.yml @@ -103,6 +103,7 @@ pt: must_accept_terms: "Você deve aceitar os termos e condições" save: "Salvar" gender_input: + label: "Gender" man: "Homem" woman: "Mulher" change_password: @@ -233,8 +234,6 @@ pt: credit_label: 'Digite a quantia a ser creditada' confirm_credit_label: 'Confirme a quantia a ser creditada' generate_a_refund_invoice: "Gerar uma fatura de reembolso" - creation_date_for_the_refund: "Data de criação de reembolso" - creation_date_is_required: "Data de criação é obrigatório." description_optional: "Descrição (opcional):" will_appear_on_the_refund_invoice: "Aparecerá na fatura de reembolso." to_credit: 'Crédito' @@ -540,3 +539,6 @@ pt: show_reserved_uniq: "Show only slots with reservations" machine: machine_uncategorized: "Uncategorized machines" + form_unsaved_list: + save_reminder: "Do not forget to save your changes" + cancel: "Cancel" diff --git a/config/locales/app.shared.zu.yml b/config/locales/app.shared.zu.yml index 160d0576c..cd69f2292 100644 --- a/config/locales/app.shared.zu.yml +++ b/config/locales/app.shared.zu.yml @@ -103,6 +103,7 @@ zu: must_accept_terms: "crwdns28700:0crwdne28700:0" save: "crwdns28702:0crwdne28702:0" gender_input: + label: "crwdns37601:0crwdne37601:0" man: "crwdns28704:0crwdne28704:0" woman: "crwdns28706:0crwdne28706:0" change_password: @@ -233,8 +234,6 @@ zu: credit_label: 'crwdns29096:0crwdne29096:0' confirm_credit_label: 'crwdns29098:0crwdne29098:0' generate_a_refund_invoice: "crwdns29100:0crwdne29100:0" - creation_date_for_the_refund: "crwdns29102:0crwdne29102:0" - creation_date_is_required: "crwdns29104:0crwdne29104:0" description_optional: "crwdns29106:0crwdne29106:0" will_appear_on_the_refund_invoice: "crwdns29108:0crwdne29108:0" to_credit: 'crwdns29110:0crwdne29110:0' @@ -540,3 +539,6 @@ zu: show_reserved_uniq: "crwdns36249:0crwdne36249:0" machine: machine_uncategorized: "crwdns36219:0crwdne36219:0" + form_unsaved_list: + save_reminder: "crwdns37471:0crwdne37471:0" + cancel: "crwdns37473:0crwdne37473:0" diff --git a/config/locales/de.yml b/config/locales/de.yml index 1e4c10649..b3f2de629 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -433,6 +433,8 @@ de: schedule_deadline: "Sie müssen den Scheck zur %{DATE} -Frist einlösen, für den Zeitplan %{REFERENCE}" notify_admin_payment_schedule_transfer_deadline: schedule_deadline: "Sie müssen das Lastschriftverfahren für die %{DATE} -Frist bestätigen, für Zeitplan %{REFERENCE}" + notify_member_reservation_limit_reached: + limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation." notify_admin_user_supporting_document_files_created: supporting_document_files_uploaded: "Supporting document uploaded by member %{NAME}." notify_admin_user_supporting_document_files_updated: @@ -519,6 +521,7 @@ de: availability: "The availaility doesn't exist" full: "The slot is already fully reserved" deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start" + limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription. Please adjust your reservation." restricted: "This availability is restricted for subscribers" plan: "This subscription plan is disabled" plan_group: "This subscription plan is reserved for members of group %{GROUP}" diff --git a/config/locales/devise.pt.yml b/config/locales/devise.pt.yml old mode 100755 new mode 100644 diff --git a/config/locales/devise.zu.yml b/config/locales/devise.zu.yml index 805d9dc73..51e859cd4 100644 --- a/config/locales/devise.zu.yml +++ b/config/locales/devise.zu.yml @@ -2,62 +2,62 @@ zu: devise: confirmations: - confirmed: "crwdns3771:0crwdne3771:0" - send_instructions: "crwdns3773:0crwdne3773:0" - send_paranoid_instructions: "crwdns3775:0crwdne3775:0" + confirmed: "crwdns37497:0crwdne37497:0" + send_instructions: "crwdns37499:0crwdne37499:0" + send_paranoid_instructions: "crwdns37501:0crwdne37501:0" failure: - already_authenticated: "crwdns3777:0crwdne3777:0" - inactive: "crwdns3779:0crwdne3779:0" - invalid: "crwdns3781:0crwdne3781:0" - locked: "crwdns3783:0crwdne3783:0" - last_attempt: "crwdns3785:0crwdne3785:0" - not_found_in_database: "crwdns3787:0crwdne3787:0" - timeout: "crwdns3789:0crwdne3789:0" - unauthenticated: "crwdns3791:0crwdne3791:0" - unconfirmed: "crwdns19573:0crwdne19573:0" + already_authenticated: "crwdns37503:0crwdne37503:0" + inactive: "crwdns37505:0crwdne37505:0" + invalid: "crwdns37507:0crwdne37507:0" + locked: "crwdns37509:0crwdne37509:0" + last_attempt: "crwdns37511:0crwdne37511:0" + not_found_in_database: "crwdns37513:0crwdne37513:0" + timeout: "crwdns37515:0crwdne37515:0" + unauthenticated: "crwdns37517:0crwdne37517:0" + unconfirmed: "crwdns37519:0crwdne37519:0" mailer: confirmation_instructions: - action: "crwdns20182:0crwdne20182:0" - instruction: "crwdns20184:0crwdne20184:0" - subject: "crwdns3799:0crwdne3799:0" + action: "crwdns37521:0crwdne37521:0" + instruction: "crwdns37523:0crwdne37523:0" + subject: "crwdns37525:0crwdne37525:0" reset_password_instructions: - action: "crwdns20186:0crwdne20186:0" - instruction: "crwdns20188:0crwdne20188:0" - ignore_otherwise: "crwdns20190:0crwdne20190:0" - subject: "crwdns3807:0crwdne3807:0" + action: "crwdns37527:0crwdne37527:0" + instruction: "crwdns37529:0crwdne37529:0" + ignore_otherwise: "crwdns37531:0crwdne37531:0" + subject: "crwdns37533:0crwdne37533:0" unlock_instructions: - subject: "crwdns3809:0crwdne3809:0" + subject: "crwdns37535:0crwdne37535:0" omniauth_callbacks: - failure: "crwdns3811:0%{kind}crwdnd3811:0%{reason}crwdne3811:0" - success: "crwdns3813:0%{kind}crwdne3813:0" + failure: "crwdns37537:0%{kind}crwdnd37537:0%{reason}crwdne37537:0" + success: "crwdns37539:0%{kind}crwdne37539:0" passwords: - no_token: "crwdns3815:0crwdne3815:0" - send_instructions: "crwdns3817:0crwdne3817:0" - send_paranoid_instructions: "crwdns3819:0crwdne3819:0" - updated: "crwdns3821:0crwdne3821:0" - updated_not_active: "crwdns3823:0crwdne3823:0" + no_token: "crwdns37541:0crwdne37541:0" + send_instructions: "crwdns37543:0crwdne37543:0" + send_paranoid_instructions: "crwdns37545:0crwdne37545:0" + updated: "crwdns37547:0crwdne37547:0" + updated_not_active: "crwdns37549:0crwdne37549:0" registrations: - destroyed: "crwdns3825:0crwdne3825:0" - signed_up: "crwdns3827:0crwdne3827:0" - signed_up_but_inactive: "crwdns3829:0crwdne3829:0" - signed_up_but_locked: "crwdns3831:0crwdne3831:0" - signed_up_but_unconfirmed: "crwdns3833:0crwdne3833:0" - update_needs_confirmation: "crwdns3835:0crwdne3835:0" - updated: "crwdns3837:0crwdne3837:0" + destroyed: "crwdns37551:0crwdne37551:0" + signed_up: "crwdns37553:0crwdne37553:0" + signed_up_but_inactive: "crwdns37555:0crwdne37555:0" + signed_up_but_locked: "crwdns37557:0crwdne37557:0" + signed_up_but_unconfirmed: "crwdns37559:0crwdne37559:0" + update_needs_confirmation: "crwdns37561:0crwdne37561:0" + updated: "crwdns37563:0crwdne37563:0" sessions: - signed_in: "crwdns3839:0crwdne3839:0" - signed_out: "crwdns3841:0crwdne3841:0" + signed_in: "crwdns37565:0crwdne37565:0" + signed_out: "crwdns37567:0crwdne37567:0" unlocks: - send_instructions: "crwdns3843:0crwdne3843:0" - send_paranoid_instructions: "crwdns3845:0crwdne3845:0" - unlocked: "crwdns3847:0crwdne3847:0" + send_instructions: "crwdns37569:0crwdne37569:0" + send_paranoid_instructions: "crwdns37571:0crwdne37571:0" + unlocked: "crwdns37573:0crwdne37573:0" errors: messages: - already_confirmed: "crwdns19575:0crwdne19575:0" - confirmation_period_expired: "crwdns3851:0%{period}crwdne3851:0" - expired: "crwdns3853:0crwdne3853:0" - not_found: "crwdns19577:0crwdne19577:0" - not_locked: "crwdns3857:0crwdne3857:0" + already_confirmed: "crwdns37575:0crwdne37575:0" + confirmation_period_expired: "crwdns37577:0%{period}crwdne37577:0" + expired: "crwdns37579:0crwdne37579:0" + not_found: "crwdns37581:0crwdne37581:0" + not_locked: "crwdns37583:0crwdne37583:0" not_saved: - one: "crwdns3859:1%{resource}crwdne3859:1" - other: "crwdns3859:5%{count}crwdnd3859:5%{resource}crwdne3859:5" + one: "crwdns37585:1%{resource}crwdne37585:1" + other: "crwdns37585:5%{count}crwdnd37585:5%{resource}crwdne37585:5" diff --git a/config/locales/en.yml b/config/locales/en.yml index 44b0a59ca..0ed2ff13f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -433,6 +433,8 @@ en: schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}" notify_admin_payment_schedule_transfer_deadline: schedule_deadline: "You must confirm the bank direct debit for the %{DATE} deadline, for schedule %{REFERENCE}" + notify_member_reservation_limit_reached: + limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation." notify_admin_user_supporting_document_files_created: supporting_document_files_uploaded: "Supporting document uploaded by member %{NAME}." notify_admin_user_supporting_document_files_updated: @@ -519,6 +521,7 @@ en: availability: "The availaility doesn't exist" full: "The slot is already fully reserved" deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start" + limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription. Please adjust your reservation." restricted: "This availability is restricted for subscribers" plan: "This subscription plan is disabled" plan_group: "This subscription plan is reserved for members of group %{GROUP}" diff --git a/config/locales/es.yml b/config/locales/es.yml index c5b568b26..6998d5048 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -433,6 +433,8 @@ es: schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}" notify_admin_payment_schedule_transfer_deadline: schedule_deadline: "You must confirm the bank direct debit for the %{DATE} deadline, for schedule %{REFERENCE}" + notify_member_reservation_limit_reached: + limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation." notify_admin_user_supporting_document_files_created: supporting_document_files_uploaded: "Supporting document uploaded by member %{NAME}." notify_admin_user_supporting_document_files_updated: @@ -519,6 +521,7 @@ es: availability: "The availaility doesn't exist" full: "The slot is already fully reserved" deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start" + limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription. Please adjust your reservation." restricted: "This availability is restricted for subscribers" plan: "This subscription plan is disabled" plan_group: "This subscription plan is reserved for members of group %{GROUP}" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 9190d103d..3f34fa9da 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -433,6 +433,8 @@ fr: schedule_deadline: "Vous devez encaisser le chèque de l'échéance du %{DATE}, pour l'échéancier %{REFERENCE}" notify_admin_payment_schedule_transfer_deadline: schedule_deadline: "Vous devez confirmer le prélèvement bancaire pour l'échéance du %{DATE} , pour l'échéancier %{REFERENCE}" + notify_member_reservation_limit_reached: + limit_reached: "Pour le %{DATE}, vous avez atteint votre limite quotidienne de %{HOURS} heures de réservation de la %{ITEM}." notify_admin_user_supporting_document_files_created: supporting_document_files_uploaded: "Le membre %{NAME} a téléversé un nouveau justificatif." notify_admin_user_supporting_document_files_updated: @@ -519,6 +521,7 @@ fr: availability: "La disponibilité n'existe pas" full: "Le créneau est déjà entièrement réservé" deadline: "Vous ne pouvez pas réserver un créneau %{MINUTES} minutes avant son début" + limit_reached: "Vous avez atteint la limite de réservation de %{HOURS}H par jour pour la %{RESERVABLE}, pour votre abonnement actuel. Merci d'ajuster votre réservation." restricted: "Cette disponibilité n'est disponible que pour les abonnés" plan: "Cette formule d'abonnement est désactivé" plan_group: "Cette formule d'abonnement est réservée aux membres du groupe %{GROUP}" diff --git a/config/locales/mails.de.yml b/config/locales/mails.de.yml index bf6cd16b9..1de187d70 100644 --- a/config/locales/mails.de.yml +++ b/config/locales/mails.de.yml @@ -375,6 +375,10 @@ de: remember: "Gemäß Ihrem Zahlungsplan von %{REFERENCE} wurde zum %{DATE} eine Belastung der Karte in Höhe von %{AMOUNT} geplant." date: "Dies ist eine Erinnerung zur Prüfung, ob das Bankkonto erfolgreich belastet werden konnte." confirm: "Bitte bestätigen Sie den Erhalt des Guthabens in Ihrer Zahlungsverwaltung, damit die entsprechende Rechnung generiert werden kann." + notify_member_reservation_limit_reached: + subject: "Daily reservation limit reached" + body: + limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation." notify_admin_user_supporting_document_files_created: subject: "Supporting documents uploaded by a member" body: diff --git a/config/locales/mails.en.yml b/config/locales/mails.en.yml index 9cf5f2f1e..6151da3c6 100644 --- a/config/locales/mails.en.yml +++ b/config/locales/mails.en.yml @@ -375,6 +375,10 @@ en: remember: "In accordance with your %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}." date: "This is a reminder to verify that the direct bank debit was successfull." confirm: "Please confirm the receipt of funds in your payment schedule management interface, so that the corresponding invoice will be generated." + notify_member_reservation_limit_reached: + subject: "Daily reservation limit reached" + body: + limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation." notify_admin_user_supporting_document_files_created: subject: "Supporting documents uploaded by a member" body: diff --git a/config/locales/mails.es.yml b/config/locales/mails.es.yml index de3a39772..338ae9812 100644 --- a/config/locales/mails.es.yml +++ b/config/locales/mails.es.yml @@ -375,6 +375,10 @@ es: remember: "In accordance with your %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}." date: "This is a reminder to verify that the direct bank debit was successfull." confirm: "Please confirm the receipt of funds in your payment schedule management interface, so that the corresponding invoice will be generated." + notify_member_reservation_limit_reached: + subject: "Daily reservation limit reached" + body: + limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation." notify_admin_user_supporting_document_files_created: subject: "Supporting documents uploaded by a member" body: diff --git a/config/locales/mails.fr.yml b/config/locales/mails.fr.yml index b6d51198b..0517ad9e2 100644 --- a/config/locales/mails.fr.yml +++ b/config/locales/mails.fr.yml @@ -375,6 +375,10 @@ fr: remember: "Conformément à l'échéancier de paiement %{REFERENCE}, une échéance de %{AMOUNT} était prévu pour être prélevée le %{DATE}." date: "Ceci est un rappel pour vérifier que le prélèvement bancaire a bien été effectué." confirm: "Veuillez confirmer la réception des fonds dans votre interface de gestion des échéanciers de paiement, afin que la facture correspondante soit générée." + notify_member_reservation_limit_reached: + subject: "Limite de réservation quotidienne atteinte" + body: + limit_reached: "Pour le %{DATE}, vous avez atteint votre limite quotidienne de %{HOURS} heures de réservation de la %{ITEM}." notify_admin_user_supporting_document_files_created: subject: "Justificatif téléversé par un membre" body: diff --git a/config/locales/mails.no.yml b/config/locales/mails.no.yml index dfa2d7078..a4ed1ec5e 100644 --- a/config/locales/mails.no.yml +++ b/config/locales/mails.no.yml @@ -375,6 +375,10 @@ remember: "In accordance with your %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}." date: "This is a reminder to verify that the direct bank debit was successfull." confirm: "Please confirm the receipt of funds in your payment schedule management interface, so that the corresponding invoice will be generated." + notify_member_reservation_limit_reached: + subject: "Daily reservation limit reached" + body: + limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation." notify_admin_user_supporting_document_files_created: subject: "Supporting documents uploaded by a member" body: diff --git a/config/locales/mails.pt.yml b/config/locales/mails.pt.yml index 2440db3c6..2462370ec 100644 --- a/config/locales/mails.pt.yml +++ b/config/locales/mails.pt.yml @@ -375,6 +375,10 @@ pt: remember: "De acordo com a agenda de pagamento %{REFERENCE}, %{AMOUNT} deveria ser debitado em %{DATE}." date: "Este é um lembrete para verificar se o débito bancário foi bem sucedido." confirm: "Não se esqueça de confirmar o recibo na interface de gestão de pagamento, para que a fatura correspondente seja gerada." + notify_member_reservation_limit_reached: + subject: "Daily reservation limit reached" + body: + limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation." notify_admin_user_supporting_document_files_created: subject: "Supporting documents uploaded by a member" body: diff --git a/config/locales/mails.zu.yml b/config/locales/mails.zu.yml index 5b0661463..6aca672d6 100644 --- a/config/locales/mails.zu.yml +++ b/config/locales/mails.zu.yml @@ -375,6 +375,10 @@ zu: remember: "crwdns29938:0%{REFERENCE}crwdnd29938:0%{AMOUNT}crwdnd29938:0%{DATE}crwdne29938:0" date: "crwdns29940:0crwdne29940:0" confirm: "crwdns29942:0crwdne29942:0" + notify_member_reservation_limit_reached: + subject: "crwdns37483:0crwdne37483:0" + body: + limit_reached: "crwdns37485:0%{DATE}crwdnd37485:0%{HOURS}crwdnd37485:0%{ITEM}crwdne37485:0" notify_admin_user_supporting_document_files_created: subject: "crwdns37349:0crwdne37349:0" body: diff --git a/config/locales/no.yml b/config/locales/no.yml index ea878e0b5..990c70005 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -433,6 +433,8 @@ schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}" notify_admin_payment_schedule_transfer_deadline: schedule_deadline: "You must confirm the bank direct debit for the %{DATE} deadline, for schedule %{REFERENCE}" + notify_member_reservation_limit_reached: + limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation." notify_admin_user_supporting_document_files_created: supporting_document_files_uploaded: "Supporting document uploaded by member %{NAME}." notify_admin_user_supporting_document_files_updated: @@ -519,6 +521,7 @@ availability: "The availaility doesn't exist" full: "The slot is already fully reserved" deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start" + limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription. Please adjust your reservation." restricted: "This availability is restricted for subscribers" plan: "This subscription plan is disabled" plan_group: "This subscription plan is reserved for members of group %{GROUP}" diff --git a/config/locales/pt.yml b/config/locales/pt.yml index ad4cc56b1..8586f8c2c 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -433,6 +433,8 @@ pt: schedule_deadline: "Você deve realizar a verificação para a data limite de %{DATE} para agendar %{REFERENCE}" notify_admin_payment_schedule_transfer_deadline: schedule_deadline: "Você deve realizar a verificação do débito para a data limite de %{DATE}, para o agendamento %{REFERENCE}" + notify_member_reservation_limit_reached: + limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation." notify_admin_user_supporting_document_files_created: supporting_document_files_uploaded: "Supporting document uploaded by member %{NAME}." notify_admin_user_supporting_document_files_updated: @@ -519,6 +521,7 @@ pt: availability: "The availaility doesn't exist" full: "The slot is already fully reserved" deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start" + limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription. Please adjust your reservation." restricted: "This availability is restricted for subscribers" plan: "This subscription plan is disabled" plan_group: "This subscription plan is reserved for members of group %{GROUP}" diff --git a/config/locales/zu.yml b/config/locales/zu.yml index 1b77b8e2f..760884db6 100644 --- a/config/locales/zu.yml +++ b/config/locales/zu.yml @@ -433,6 +433,8 @@ zu: schedule_deadline: "crwdns21120:0%{DATE}crwdnd21120:0%{REFERENCE}crwdne21120:0" notify_admin_payment_schedule_transfer_deadline: schedule_deadline: "crwdns22305:0%{DATE}crwdnd22305:0%{REFERENCE}crwdne22305:0" + notify_member_reservation_limit_reached: + limit_reached: "crwdns37481:0%{DATE}crwdnd37481:0%{HOURS}crwdnd37481:0%{ITEM}crwdne37481:0" notify_admin_user_supporting_document_files_created: supporting_document_files_uploaded: "crwdns37341:0%{NAME}crwdne37341:0" notify_admin_user_supporting_document_files_updated: @@ -519,6 +521,7 @@ zu: availability: "crwdns36269:0crwdne36269:0" full: "crwdns36271:0crwdne36271:0" deadline: "crwdns36273:0%{MINUTES}crwdne36273:0" + limit_reached: "crwdns37587:0%{HOURS}crwdnd37587:0%{RESERVABLE}crwdne37587:0" restricted: "crwdns36275:0crwdne36275:0" plan: "crwdns36277:0crwdne36277:0" plan_group: "crwdns37207:0%{GROUP}crwdne37207:0" diff --git a/db/migrate/20230307123611_add_limiting_to_plan.rb b/db/migrate/20230307123611_add_limiting_to_plan.rb new file mode 100644 index 000000000..5fd95b875 --- /dev/null +++ b/db/migrate/20230307123611_add_limiting_to_plan.rb @@ -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 diff --git a/db/migrate/20230307123841_create_plan_limitations.rb b/db/migrate/20230307123841_create_plan_limitations.rb new file mode 100644 index 000000000..9b7e2a3d2 --- /dev/null +++ b/db/migrate/20230307123841_create_plan_limitations.rb @@ -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 diff --git a/db/migrate/20230315095054_add_machine_visibility_to_plan.rb b/db/migrate/20230315095054_add_machine_visibility_to_plan.rb new file mode 100644 index 000000000..33639544d --- /dev/null +++ b/db/migrate/20230315095054_add_machine_visibility_to_plan.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index a0ac87761..629437c52 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_03_09_094535) do +ActiveRecord::Schema.define(version: 2023_03_15_095054) do # These are extensions that must be enabled in order to support this database enable_extension "fuzzystrmatch" @@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do enable_extension "unaccent" create_table "abuses", id: :serial, force: :cascade do |t| - t.integer "signaled_id" t.string "signaled_type" + t.integer "signaled_id" t.string "first_name" t.string "last_name" t.string "email" @@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do t.string "locality" t.string "country" t.string "postal_code" - t.integer "placeable_id" t.string "placeable_type" + t.integer "placeable_id" t.datetime "created_at" t.datetime "updated_at" end @@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do end create_table "assets", id: :serial, force: :cascade do |t| - t.integer "viewable_id" t.string "viewable_type" + t.integer "viewable_id" t.string "attachment" t.string "type" t.datetime "created_at" @@ -164,10 +164,10 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do create_table "cart_item_event_reservation_tickets", force: :cascade do |t| t.integer "booked" + t.bigint "event_price_category_id" t.bigint "cart_item_event_reservation_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.bigint "event_price_category_id" t.index ["cart_item_event_reservation_id"], name: "index_cart_item_tickets_on_cart_item_event_reservation" t.index ["event_price_category_id"], name: "index_cart_item_tickets_on_event_price_category" end @@ -282,8 +282,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do end create_table "credits", id: :serial, force: :cascade do |t| - t.integer "creditable_id" t.string "creditable_type" + t.integer "creditable_id" t.integer "plan_id" t.integer "hours" t.datetime "created_at" @@ -546,15 +546,15 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do create_table "notifications", id: :serial, force: :cascade do |t| t.integer "receiver_id" - t.integer "attached_object_id" t.string "attached_object_type" + t.integer "attached_object_id" t.integer "notification_type_id" t.boolean "is_read", default: false t.datetime "created_at" t.datetime "updated_at" t.string "receiver_type" t.boolean "is_send", default: false - t.jsonb "meta_data", default: {} + t.jsonb "meta_data", default: "{}" t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id" t.index ["receiver_id"], name: "index_notifications_on_receiver_id" end @@ -740,13 +740,14 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do end create_table "plan_limitations", force: :cascade do |t| - t.bigint "plan_id" - t.string "limitable_type" - t.bigint "limitable_id" + t.bigint "plan_id", null: false + t.string "limitable_type", null: false + t.bigint "limitable_id", null: false t.integer "limit", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["limitable_type", "limitable_id"], name: "index_plan_limitations_on_limitable_type_and_limitable_id" + t.index ["plan_id", "limitable_id", "limitable_type"], name: "index_plan_limitations_on_plan_and_limitable", unique: true t.index ["plan_id"], name: "index_plan_limitations_on_plan_id" end @@ -770,6 +771,7 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do t.boolean "monthly_payment" t.bigint "plan_category_id" t.boolean "limiting" + t.integer "machines_visibility" t.index ["group_id"], name: "index_plans_on_group_id" t.index ["plan_category_id"], name: "index_plans_on_plan_category_id" end @@ -811,15 +813,14 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do t.text "conditions" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index "btrim(lower((name)::text))", name: "index_price_categories_on_TRIM_BOTH_FROM_LOWER_name", unique: true t.index ["name"], name: "index_price_categories_on_name", unique: true end create_table "prices", id: :serial, force: :cascade do |t| t.integer "group_id" t.integer "plan_id" - t.integer "priceable_id" t.string "priceable_type" + t.integer "priceable_id" t.integer "amount" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -983,8 +984,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do t.text "message" t.datetime "created_at" t.datetime "updated_at" - t.integer "reservable_id" t.string "reservable_type" + t.integer "reservable_id" t.integer "nb_reserve_places" t.integer "statistic_profile_id" t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id" @@ -993,8 +994,8 @@ ActiveRecord::Schema.define(version: 2023_03_09_094535) do create_table "roles", id: :serial, force: :cascade do |t| t.string "name" - t.integer "resource_id" t.string "resource_type" + t.integer "resource_id" t.datetime "created_at" t.datetime "updated_at" t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" diff --git a/db/seeds/notification_types.rb b/db/seeds/notification_types.rb index 43d213005..8dbe8dc68 100644 --- a/db/seeds/notification_types.rb +++ b/db/seeds/notification_types.rb @@ -23,3 +23,11 @@ unless NotificationType.find_by(name: 'notify_admin_order_is_paid') is_configurable: true ) end + +unless NotificationType.find_by(name: 'notify_member_reservation_limit_reached') + NotificationType.create!( + name: 'notify_member_reservation_limit_reached', + category: 'agenda', + is_configurable: false + ) +end diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 87c5faba0..4df47c23c 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -418,7 +418,15 @@ Setting.set('visibility_yearly', 3) unless Setting.find_by(name: 'visibility_yea Setting.set('visibility_others', 1) unless Setting.find_by(name: 'visibility_others').try(:value) -Setting.set('reservation_deadline', 0) unless Setting.find_by(name: 'reservation_deadline').try(:value) +reservation_deadline = Setting.get('reservation_deadline') || 0 + +Setting.set('machine_reservation_deadline', reservation_deadline) unless Setting.find_by(name: 'machine_reservation_deadline').try(:value) + +Setting.set('training_reservation_deadline', reservation_deadline) unless Setting.find_by(name: 'training_reservation_deadline').try(:value) + +Setting.set('event_reservation_deadline', reservation_deadline) unless Setting.find_by(name: 'event_reservation_deadline').try(:value) + +Setting.set('space_reservation_deadline', reservation_deadline) unless Setting.find_by(name: 'space_reservation_deadline').try(:value) Setting.set('display_name_enable', false) unless Setting.find_by(name: 'display_name_enable').try(:value) diff --git a/package.json b/package.json index 05906af42..bc199b30f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "5.8.2", + "version": "5.9.0", "description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.", "keywords": [ "fablab", @@ -161,7 +161,7 @@ "react-cool-onclickoutside": "^1.7.0", "react-custom-events": "^1.1.1", "react-dom": "^17.0.2", - "react-hook-form": "^7.30.0", + "react-hook-form": "~7.31.3", "react-i18next": "^11.15.6", "react-modal": "^3.16.1", "react-select": "^5.3.2", @@ -183,7 +183,7 @@ "ui-select": "0.19", "underscore": "1.12", "use-immer": "^0.5.1", - "webpack": "5.72.0", + "webpack": "5.76.0", "webpack-assets-manifest": "5", "webpack-cli": "4", "webpack-merge": "5", diff --git a/scripts/translations/upload.sh b/scripts/translations/upload.sh index 00c614bea..eafb01c49 100755 --- a/scripts/translations/upload.sh +++ b/scripts/translations/upload.sh @@ -34,7 +34,7 @@ list_files() { update_file() { # params: FILE_ID, STORAGE_ID - curl -s -X PUT "https://api.crowdin.com/api/v2/projects/$PROJECT_ID/files/$1" -H "$(authorization)" -H "Content-Type: application/json" -d "{ \"storageId\": $2, \"updateOption\": \"keep_translations_and_approvals\" }" + curl -s -X PUT "https://api.crowdin.com/api/v2/projects/$PROJECT_ID/files/$1" -H "$(authorization)" -H "Content-Type: application/json" -d "{ \"storageId\": $2 }" } find_file_id() { diff --git a/test/fixtures/orders.yml b/test/fixtures/orders.yml index 07071d2ca..6ece87147 100644 --- a/test/fixtures/orders.yml +++ b/test/fixtures/orders.yml @@ -150,8 +150,8 @@ order_15: reference: '005900-10-22' state: ready total: 3000 - created_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - updated_at: <%= DateTime.current.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> wallet_amount: wallet_transaction_id: payment_method: local @@ -168,8 +168,8 @@ order_16: reference: '005901-<%= DateTime.current.utc.strftime('%m') %>-<%= DateTime.current.utc.strftime('%d') %>' state: cart total: 0 - created_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - updated_at: <%= DateTime.current.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> wallet_amount: wallet_transaction_id: payment_method: @@ -186,8 +186,8 @@ order_17: reference: '005902-<%= DateTime.current.utc.strftime('%m') %>-<%= DateTime.current.utc.strftime('%d') %>' state: cart total: 0 - created_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - updated_at: <%= DateTime.current.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> wallet_amount: wallet_transaction_id: payment_method: @@ -204,8 +204,8 @@ order_18: reference: '005902-<%= DateTime.current.utc.strftime('%m') %>-<%= DateTime.current.utc.strftime('%d') %>' state: cart total: 500 - created_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - updated_at: <%= DateTime.current.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> wallet_amount: wallet_transaction_id: payment_method: @@ -222,8 +222,8 @@ order_19: reference: '005903-<%= DateTime.current.utc.strftime('%m') %>-<%= DateTime.current.utc.strftime('%d') %>' state: cart total: 261500 - created_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - updated_at: <%= DateTime.current.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> wallet_amount: wallet_transaction_id: payment_method: @@ -240,8 +240,8 @@ order_20: reference: '005904-<%= DateTime.current.utc.strftime('%m') %>-<%= DateTime.current.utc.strftime('%d') %>' state: cart total: 262500 - created_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - updated_at: <%= DateTime.current.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> wallet_amount: wallet_transaction_id: payment_method: diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index dc1a21bc7..c937d613c 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -1,3 +1,4 @@ +# admin without subscription user_1: id: 1 username: admin @@ -30,6 +31,7 @@ user_1: merged_at: is_allow_newsletter: true +# member without subscription user_2: id: 2 username: jdupond @@ -62,6 +64,7 @@ user_2: merged_at: is_allow_newsletter: true +# member with 1 month subscription (plan 2/standard) user_3: id: 3 username: pdurand @@ -94,6 +97,7 @@ user_3: merged_at: is_allow_newsletter: false +# member with 1 month subscription (plan 3/students) user_4: id: 4 username: kdumas @@ -126,6 +130,7 @@ user_4: merged_at: is_allow_newsletter: false +# member with 10€ on wallet user_5: id: 5 username: vlonchamp @@ -158,6 +163,7 @@ user_5: merged_at: is_allow_newsletter: true +# partner of plan 2 user_6: id: 6 username: GilbertPartenaire @@ -190,6 +196,7 @@ user_6: merged_at: is_allow_newsletter: true +# member with 255€ on wallet user_7: id: 7 username: lseguin @@ -286,6 +293,7 @@ user_9: merged_at: is_allow_newsletter: true +# member with 1 year subscription user_10: id: 10 username: acamus diff --git a/test/frontend/__fixtures__/settings.ts b/test/frontend/__fixtures__/settings.ts index 430b439d2..03dd134be 100644 --- a/test/frontend/__fixtures__/settings.ts +++ b/test/frontend/__fixtures__/settings.ts @@ -737,10 +737,28 @@ export const settings: Array = [ localized: 'éviter la génération de factures à 0' }, { - name: 'reservation_deadline', + name: 'machine_reservation_deadline', value: '0', - last_update: '2022-11-29T21:02:47-0300', - localized: "Empêcher la réservation avant qu'elle ne commence" + last_update: '2023-03-01T16:28:23-0300', + localized: 'Machine prior period (minutes)' + }, + { + name: 'training_reservation_deadline', + value: '0', + last_update: '2023-03-01T16:28:23-0300', + localized: 'Training prior period (minutes)' + }, + { + name: 'event_reservation_deadline', + value: '0', + last_update: '2023-03-01T16:28:23-0300', + localized: 'Event prior period (minutes)' + }, + { + name: 'space_reservation_deadline', + value: '0', + last_update: '2023-03-01T16:28:23-0300', + localized: 'Space prior period (minutes)' }, { name: 'invoice_VAT-name', diff --git a/test/frontend/components/plans/plan-form.test.tsx b/test/frontend/components/plans/plan-form.test.tsx index 3423b8afe..1ef115d8d 100644 --- a/test/frontend/components/plans/plan-form.test.tsx +++ b/test/frontend/components/plans/plan-form.test.tsx @@ -7,6 +7,7 @@ import userEvent from '@testing-library/user-event'; import plans from '../../__fixtures__/plans'; import machines from '../../__fixtures__/machines'; import { tiptapEvent } from '../../__lib__/tiptap'; +import { uiRouter } from '../../__lib__/ui-router'; describe('PlanForm', () => { const onError = jest.fn(); @@ -14,7 +15,7 @@ describe('PlanForm', () => { const beforeSubmit = jest.fn(); test('render create PlanForm', async () => { - render(); + render(); await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ })); expect(screen.getByLabelText(/app.admin.plan_form.name/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.plan_form.transversal/)).toBeInTheDocument(); @@ -31,11 +32,11 @@ describe('PlanForm', () => { expect(screen.getByLabelText(/app.admin.plan_form.period/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.plan_form.partner_plan/)).toBeInTheDocument(); expect(screen.queryByTestId('plan-pricing-form')).toBeNull(); - expect(screen.getByRole('button', { name: /app.admin.plan_form.ACTION_plan/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /app.admin.plan_form.save/ })).toBeInTheDocument(); }); test('create new plan', async () => { - render(); + render(); await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ })); const user = userEvent.setup(); // base_name @@ -66,7 +67,7 @@ describe('PlanForm', () => { // advanced_accounting_attributes.analytical_section fireEvent.change(screen.getByLabelText(/app.admin.advanced_accounting_form.analytical_section/), { target: { value: '9B20A' } }); // send the form - fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.ACTION_plan/ })); + fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.save/ })); await waitFor(() => { const expected: Plan = { base_name: 'Test Plan', @@ -98,7 +99,7 @@ describe('PlanForm', () => { test('render update PlanForm with partner', async () => { const plan = plans[1]; - render(); + render(); await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_pricing_form.copy_prices_from/ })); expect(screen.getByLabelText(/app.admin.plan_form.name/)).toBeInTheDocument(); expect(screen.queryByLabelText(/app.admin.plan_form.transversal/)).toBeNull(); @@ -119,18 +120,18 @@ describe('PlanForm', () => { expect(screen.getByLabelText(/app.admin.plan_form.notified_partner/)).toBeInTheDocument(); expect(screen.getByText(/app.admin.plan_form.alert_partner_notification/)).toBeInTheDocument(); expect(screen.getByTestId('plan-pricing-form')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /app.admin.plan_form.ACTION_plan/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /app.admin.plan_form.save/ })).toBeInTheDocument(); }); test('selecting transversal plan disables group select', async () => { - render(); + render(); await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ })); fireEvent.click(screen.getByRole('switch', { name: /app.admin.plan_form.transversal/ })); expect(screen.queryByRole('combobox', { name: /app.admin.plan_form.group/ })).toBeNull(); }); test('selecting partner plan shows partner selection', async () => { - render(); + render(); await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ })); fireEvent.click(screen.getByRole('switch', { name: /app.admin.plan_form.partner_plan/ })); expect(screen.getByLabelText(/app.admin.plan_form.notified_partner/)); @@ -138,7 +139,7 @@ describe('PlanForm', () => { }); test('creating a new partner selects him by default', async () => { - render(); + render(); await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ })); fireEvent.click(screen.getByRole('switch', { name: /app.admin.plan_form.partner_plan/ })); fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.new_user/ })); @@ -157,12 +158,12 @@ describe('PlanForm', () => { test('update plan prices', async () => { const plan = plans[1]; const machine = machines[1]; - render(); + render(); await waitFor(() => screen.getByLabelText(new RegExp(machine.name))); // update machine price fireEvent.change(screen.getByLabelText(new RegExp(machine.name)), { target: { value: 42.42 } }); // send the form - fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.ACTION_plan/ })); + fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.save/ })); await waitFor(() => { const expected = { prices_attributes: expect.arrayContaining([{ diff --git a/test/helpers/invoice_helper.rb b/test/helpers/invoice_helper.rb index 4fb6941fe..2f2cf27ef 100644 --- a/test/helpers/invoice_helper.rb +++ b/test/helpers/invoice_helper.rb @@ -4,8 +4,8 @@ module InvoiceHelper # Force the invoice generation worker to run NOW and check the resulting file generated. # Delete the file afterwards. - # @param invoice {Invoice} - # @param &block an optional block may be provided for additional specific assertions on the invoices PDF lines + # @param invoice [Invoice] + # @yield an optional block may be provided for additional specific assertions on the invoices PDF lines def assert_invoice_pdf(invoice) assert_not_nil invoice, 'Invoice was not created' @@ -27,6 +27,43 @@ module InvoiceHelper File.delete(invoice.file) end + # @param customer [User] + # @param operator [User] + # @return [Invoice] saved + def sample_reservation_invoice(customer, operator) + machine = Machine.first + slot = Availabilities::AvailabilitiesService.new(operator) + .machines([machine], customer, { start: Time.current, end: 1.year.from_now }) + .find { |s| !s.full?(machine) } + reservation = Reservation.new( + reservable: machine, + slots_reservations: [SlotsReservation.new({ slot_id: slot.id })], + statistic_profile: customer.statistic_profile + ) + reservation.save + invoice = Invoice.new( + invoicing_profile: customer.invoicing_profile, + statistic_profile: customer.statistic_profile, + operator_profile: operator.invoicing_profile, + payment_method: '', + invoice_items: [InvoiceItem.new( + amount: 1000, + description: "reservation #{machine.name}", + object: reservation, + main: true + )] + ) + unless operator.privileged? + invoice.payment_method = 'card' + invoice.payment_gateway_object = PaymentGatewayObject.new( + gateway_object_id: 'pi_3LpALs2sOmf47Nz91QyFI7nP', + gateway_object_type: 'Stripe::PaymentIntent' + ) + end + invoice.save + invoice + end + private def generate_pdf(invoice) diff --git a/test/helpers/payment_schedule_helper.rb b/test/helpers/payment_schedule_helper.rb new file mode 100644 index 000000000..90ba5702b --- /dev/null +++ b/test/helpers/payment_schedule_helper.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Provides methods to help testing payment schedules +module PaymentScheduleHelper + # Force the payment schedule generation worker to run NOW and check the resulting file generated. + # Delete the file afterwards. + # @param schedule [PaymentSchedule] + def assert_schedule_pdf(schedule) + assert_not_nil schedule, 'Schedule was not created' + + generate_schedule_pdf(schedule) + + assert File.exist?(schedule.file), 'Schedule PDF was not generated' + + File.delete(schedule.file) + end + + # @param customer [User] + # @param operator [User] + # @return [PaymentSchedule] saved + def sample_schedule(customer, operator) + plan = plans(:plan_schedulable) + subscription = Subscription.new(plan: plan, statistic_profile_id: customer.statistic_profile, start_at: Time.current) + subscription.save + options = { payment_method: '' } + unless operator.privileged? + options = { payment_method: 'card', payment_id: 'pi_3LpALs2sOmf47Nz91QyFI7nP', payment_type: 'Stripe::PaymentIntent' } + end + schedule = PaymentScheduleService.new.create([subscription], 113_600, customer, operator: operator, **options) + schedule.save + first_item = schedule.ordered_items.first + PaymentScheduleService.new.generate_invoice(first_item, **options) + first_item.update(state: 'paid', payment_method: operator.privileged? ? 'check' : 'card') + schedule + end + + private + + def generate_schedule_pdf(schedule) + schedule_worker = PaymentScheduleWorker.new + schedule_worker.perform(schedule.id) + end +end diff --git a/test/integration/invoices/as_admin_test.rb b/test/integration/invoices/as_admin_test.rb index dedbb38c5..e07703c9d 100644 --- a/test/integration/invoices/as_admin_test.rb +++ b/test/integration/invoices/as_admin_test.rb @@ -67,24 +67,4 @@ class InvoicesTest < ActionDispatch::IntegrationTest # Check footprint assert avoir.check_footprint end - - test 'admin fails generates a refund in closed period' do - date = Time.zone.parse('2015-10-01T13:09:55+01:00') - - post '/api/invoices', params: { avoir: { - avoir_date: date, - payment_method: 'cash', - description: 'Unable to refund', - invoice_id: 5, - invoice_items_ids: [5], - subscription_to_expire: false - } }.to_json, headers: default_headers - - # Check response format & status - assert_equal 422, response.status, response.body - assert_equal Mime[:json], response.content_type - - # Check the error was handled - assert_match(/#{I18n.t('errors.messages.in_closed_period')}/, response.body) - end end diff --git a/test/integration/reservations/last_minute_test.rb b/test/integration/reservations/last_minute_test.rb index d0910ea37..88b366f75 100644 --- a/test/integration/reservations/last_minute_test.rb +++ b/test/integration/reservations/last_minute_test.rb @@ -13,7 +13,7 @@ class Reservations::LastMinuteTest < ActionDispatch::IntegrationTest end test 'user cannot reserve last minute booking' do - Setting.set('reservation_deadline', '120') + Setting.set('space_reservation_deadline', '120') login_as(@user, scope: :user) @@ -44,7 +44,7 @@ class Reservations::LastMinuteTest < ActionDispatch::IntegrationTest end test 'user can reserve last minute booking' do - Setting.set('reservation_deadline', '0') + Setting.set('space_reservation_deadline', '0') login_as(@user, scope: :user) @@ -85,7 +85,7 @@ class Reservations::LastMinuteTest < ActionDispatch::IntegrationTest end test 'admin can reserve last minute booking anyway' do - Setting.set('reservation_deadline', '120') + Setting.set('space_reservation_deadline', '120') login_as(@admin, scope: :user) diff --git a/test/integration/reservations/payment_schedule_test.rb b/test/integration/reservations/payment_schedule_test.rb index deb0eb0df..df7147d6a 100644 --- a/test/integration/reservations/payment_schedule_test.rb +++ b/test/integration/reservations/payment_schedule_test.rb @@ -84,6 +84,7 @@ class Reservations::PaymentScheduleTest < ActionDispatch::IntegrationTest assert payment_schedule.check_footprint assert_equal @user_without_subscription.invoicing_profile.id, payment_schedule.invoicing_profile_id assert_equal @admin.invoicing_profile.id, payment_schedule.operator_profile_id + assert_schedule_pdf(payment_schedule) # Check the answer result = json_response(response.body) diff --git a/test/integration/reservations/restricted_test.rb b/test/integration/reservations/restricted_test.rb index 35113d87b..1fd6ac493 100644 --- a/test/integration/reservations/restricted_test.rb +++ b/test/integration/reservations/restricted_test.rb @@ -72,7 +72,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest }.to_json, headers: default_headers end - assert_equal 201, response.status + assert_equal 201, response.status, response.body assert_equal reservations_count + 1, Reservation.count assert_equal invoices_count + 1, Invoice.count @@ -105,7 +105,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest } } - assert_equal 201, response.status + assert_equal 201, response.status, response.body # Check the id availability = json_response(response.body) @@ -141,7 +141,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest }.to_json, headers: default_headers end - assert_equal 422, response.status + assert_equal 422, response.status, response.body assert_match(/availability is restricted for subscribers/, response.body) assert_equal reservations_count, Reservation.count @@ -175,7 +175,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest } } - assert_equal 201, response.status + assert_equal 201, response.status, response.body # Check the id availability = json_response(response.body) @@ -187,27 +187,25 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest slot = Availability.find(availability[:id]).slots.first # book a reservation - VCR.use_cassette('reservations_create_for_restricted_slot_forced') do - post '/api/local_payment/confirm_payment', - params: { - customer_id: @jdupont.id, - items: [ - { - reservation: { - reservable_id: 2, - reservable_type: 'Machine', - slots_reservations_attributes: [ - { - slot_id: slot.id - } - ] - } + post '/api/local_payment/confirm_payment', + params: { + customer_id: @jdupont.id, + items: [ + { + reservation: { + reservable_id: 2, + reservable_type: 'Machine', + slots_reservations_attributes: [ + { + slot_id: slot.id + } + ] } - ] - }.to_json, headers: default_headers - end + } + ] + }.to_json, headers: default_headers - assert_equal 201, response.status + assert_equal 201, response.status, response.body # Check the result result = json_response(response.body) @@ -246,7 +244,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest } } - assert_equal 201, response.status + assert_equal 201, response.status, response.body # Check the id availability = json_response(response.body) @@ -287,7 +285,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest }.to_json, headers: default_headers end - assert_equal 201, response.status + assert_equal 201, response.status, response.body # Check the result result = json_response(response.body) diff --git a/test/services/availabilities_service_test.rb b/test/services/availabilities/availabilities_service_test.rb similarity index 98% rename from test/services/availabilities_service_test.rb rename to test/services/availabilities/availabilities_service_test.rb index 35024ac86..9d9e40315 100644 --- a/test/services/availabilities_service_test.rb +++ b/test/services/availabilities/availabilities_service_test.rb @@ -3,7 +3,7 @@ require 'test_helper' # Test the service returning the availabilities for the given resources -class AvailabilitiesServiceTest < ActiveSupport::TestCase +class Availabilities::AvailabilitiesServiceTest < ActiveSupport::TestCase setup do @no_subscription = User.find_by(username: 'jdupond') @with_subscription = User.find_by(username: 'kdumas') diff --git a/test/services/availabilities/visibility_service_test.rb b/test/services/availabilities/visibility_service_test.rb new file mode 100644 index 000000000..b461ed38f --- /dev/null +++ b/test/services/availabilities/visibility_service_test.rb @@ -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, 'machines', starting, ending) + assert_datetimes_equal Time.current, window[0] + assert_datetimes_equal 48.hours.from_now, window[1] + end +end diff --git a/test/services/payment_document_service_test.rb b/test/services/payment_document_service_test.rb new file mode 100644 index 000000000..f422d8b3d --- /dev/null +++ b/test/services/payment_document_service_test.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true + +require 'test_helper' + +class PaymentDocumentServiceTest < ActiveSupport::TestCase + setup do + @admin = User.find_by(username: 'admin') + @acamus = User.find_by(username: 'acamus') + @machine = Machine.first + # From the fixtures, + # - invoice_reference = YYMMmmmX[/VL]R[/A] + # - invoice_order-nb = nnnnnn-MM-YY + end + + test 'invoice for local payment' do + invoice = sample_reservation_invoice(@acamus, @admin) + assert_equal "#{Time.current.strftime('%y%m')}001", invoice.reference + assert_equal "000023-#{Time.current.strftime('%m-%y')}", invoice.order_number + end + + test 'invoice with custom format' do + travel_to(Time.current.beginning_of_month) + Setting.set('invoice_reference', 'YYYYMMMDdddddX[/VL]R[/A]S[/E]') + Setting.set('invoice_order-nb', 'yyyy-YYYY') + invoice = sample_reservation_invoice(@acamus, @admin) + assert_equal "#{Time.current.strftime('%Y%^b%-d')}00001", invoice.reference + assert_equal "0001-#{Time.current.strftime('%Y')}", invoice.order_number + travel_back + end + + test 'invoice with other custom format' do + travel_to(Time.current.beginning_of_year) + Setting.set('invoice_reference', 'YYMDDyyyyX[/VL]R[/A]S[/E]') + Setting.set('invoice_order-nb', 'DMYYYYnnnnnn') + invoice = sample_reservation_invoice(@acamus, @admin) + assert_equal "#{Time.current.strftime('%y%-m%d')}0001", invoice.reference + assert_equal "#{Time.current.strftime('%-d%-m%Y')}000018", invoice.order_number + travel_back + end + + test 'invoice for online card payment' do + invoice = sample_reservation_invoice(@acamus, @acamus) + assert_equal "#{Time.current.strftime('%y%m')}001/VL", invoice.reference + assert_equal "000023-#{Time.current.strftime('%m-%y')}", invoice.order_number + end + + test 'refund' do + invoice = sample_reservation_invoice(@acamus, @admin) + assert_equal "#{Time.current.strftime('%y%m')}001", invoice.reference + assert_equal "000023-#{Time.current.strftime('%m-%y')}", invoice.order_number + + refund = invoice.build_avoir(payment_method: 'wallet', invoice_items_ids: invoice.invoice_items.map(&:id)) + refund.save + refund.reload + assert_equal "#{Time.current.strftime('%y%m')}002/A", refund.reference + assert_equal "000023-#{Time.current.strftime('%m-%y')}", refund.order_number + end + + test 'payment schedule' do + Setting.set('invoice_reference', 'YYMMmmmX[/VL]R[/A]S[/E]') + schedule = sample_schedule(@acamus, @admin) + assert_equal "#{Time.current.strftime('%y%m')}001/E", schedule.reference + first_item = schedule.ordered_items.first + assert_equal "#{Time.current.strftime('%y%m')}001", first_item.invoice.reference + assert_equal "000023-#{Time.current.strftime('%m-%y')}", first_item.invoice.order_number + second_item = schedule.ordered_items[1] + PaymentScheduleService.new.generate_invoice(second_item, payment_method: 'check') + assert_equal "#{Time.current.strftime('%y%m')}002", second_item.invoice.reference + assert_equal "000023-#{Time.current.strftime('%m-%y')}", second_item.invoice.order_number + third_item = schedule.ordered_items[2] + PaymentScheduleService.new.generate_invoice(third_item, payment_method: 'check') + assert_equal "#{Time.current.strftime('%y%m')}003", third_item.invoice.reference + assert_equal "000023-#{Time.current.strftime('%m-%y')}", third_item.invoice.order_number + fourth_item = schedule.ordered_items[3] + PaymentScheduleService.new.generate_invoice(fourth_item, payment_method: 'check') + assert_equal "#{Time.current.strftime('%y%m')}004", fourth_item.invoice.reference + assert_equal "000023-#{Time.current.strftime('%m-%y')}", fourth_item.invoice.order_number + fifth_item = schedule.ordered_items[2] + PaymentScheduleService.new.generate_invoice(fifth_item, payment_method: 'check') + assert_equal "#{Time.current.strftime('%y%m')}005", fifth_item.invoice.reference + assert_equal "000023-#{Time.current.strftime('%m-%y')}", fifth_item.invoice.order_number + end + + test 'order' do + cart = Cart::FindOrCreateService.new(users(:user_2)).call(nil) + cart = Cart::AddItemService.new.call(cart, Product.find_by(slug: 'panneaux-de-mdf'), 1) + Checkout::PaymentService.new.payment(cart, @admin, nil) + assert_equal "000023-#{Time.current.strftime('%m-%y')}", cart.reference # here reference = order number + assert_equal "000023-#{Time.current.strftime('%m-%y')}", cart.invoice.order_number + assert_equal "#{Time.current.strftime('%y%m')}001", cart.invoice.reference + end + + test 'multiple items logical sequence' do + Setting.set('invoice_reference', 'YYMMmmmX[/VL]R[/A]S[/E]') + + invoice = sample_reservation_invoice(@acamus, @admin) + assert_equal "#{Time.current.strftime('%y%m')}001", invoice.reference + assert_equal "000023-#{Time.current.strftime('%m-%y')}", invoice.order_number + + refund = invoice.build_avoir(payment_method: 'wallet', invoice_items_ids: invoice.invoice_items.map(&:id)) + refund.save + refund.reload + assert_equal "#{Time.current.strftime('%y%m')}002/A", refund.reference + assert_equal "000023-#{Time.current.strftime('%m-%y')}", refund.order_number + + invoice = sample_reservation_invoice(@acamus, @admin) + assert_equal "#{Time.current.strftime('%y%m')}003", invoice.reference + assert_equal "000024-#{Time.current.strftime('%m-%y')}", invoice.order_number + + invoice = sample_reservation_invoice(@acamus, @acamus) + assert_equal "#{Time.current.strftime('%y%m')}004/VL", invoice.reference + assert_equal "000025-#{Time.current.strftime('%m-%y')}", invoice.order_number + + invoice = sample_reservation_invoice(@acamus, @admin) + assert_equal "#{Time.current.strftime('%y%m')}005", invoice.reference + assert_equal "000026-#{Time.current.strftime('%m-%y')}", invoice.order_number + + invoice = sample_reservation_invoice(@acamus, @admin) + assert_equal "#{Time.current.strftime('%y%m')}006", invoice.reference + assert_equal "000027-#{Time.current.strftime('%m-%y')}", invoice.order_number + + invoice = sample_reservation_invoice(@acamus, @acamus) + assert_equal "#{Time.current.strftime('%y%m')}007/VL", invoice.reference + assert_equal "000028-#{Time.current.strftime('%m-%y')}", invoice.order_number + + invoice = sample_reservation_invoice(@acamus, @acamus) + assert_equal "#{Time.current.strftime('%y%m')}008/VL", invoice.reference + assert_equal "000029-#{Time.current.strftime('%m-%y')}", invoice.order_number + + refund = invoice.build_avoir(payment_method: 'wallet', invoice_items_ids: invoice.invoice_items.map(&:id)) + refund.save + refund.reload + assert_equal "#{Time.current.strftime('%y%m')}009/A", refund.reference + assert_equal "000029-#{Time.current.strftime('%m-%y')}", refund.order_number + + invoice = sample_reservation_invoice(@acamus, @acamus) + assert_equal "#{Time.current.strftime('%y%m')}010/VL", invoice.reference + assert_equal "000030-#{Time.current.strftime('%m-%y')}", invoice.order_number + + invoice2 = sample_reservation_invoice(@acamus, @admin) + assert_equal "#{Time.current.strftime('%y%m')}011", invoice2.reference + assert_equal "000031-#{Time.current.strftime('%m-%y')}", invoice2.order_number + + refund = invoice.build_avoir(payment_method: 'wallet', invoice_items_ids: invoice.invoice_items.map(&:id)) + refund.save + refund.reload + assert_equal "#{Time.current.strftime('%y%m')}012/A", refund.reference + assert_equal "000030-#{Time.current.strftime('%m-%y')}", refund.order_number + + refund = invoice2.build_avoir(payment_method: 'wallet', invoice_items_ids: invoice.invoice_items.map(&:id)) + refund.save + refund.reload + assert_equal "#{Time.current.strftime('%y%m')}013/A", refund.reference + assert_equal "000031-#{Time.current.strftime('%m-%y')}", refund.order_number + + schedule = sample_schedule(@acamus, @admin) + assert_equal "#{Time.current.strftime('%y%m')}001/E", schedule.reference + assert_equal "#{Time.current.strftime('%y%m')}014", schedule.ordered_items.first.invoice.reference + assert_equal "000032-#{Time.current.strftime('%m-%y')}", schedule.ordered_items.first.invoice.order_number + + schedule = sample_schedule(users(:user_2), users(:user_2)) + assert_equal "#{Time.current.strftime('%y%m')}002/E", schedule.reference + assert_equal "#{Time.current.strftime('%y%m')}015/VL", schedule.ordered_items.first.invoice.reference + assert_equal "000033-#{Time.current.strftime('%m-%y')}", schedule.ordered_items.first.invoice.order_number + + invoice = sample_reservation_invoice(@acamus, @acamus) + assert_equal "#{Time.current.strftime('%y%m')}016/VL", invoice.reference + assert_equal "000034-#{Time.current.strftime('%m-%y')}", invoice.order_number + + cart = Cart::FindOrCreateService.new(users(:user_2)).call(nil) + cart = Cart::AddItemService.new.call(cart, Product.find_by(slug: 'panneaux-de-mdf'), 1) + Checkout::PaymentService.new.payment(cart, @admin, nil) + assert_equal "000035-#{Time.current.strftime('%m-%y')}", cart.reference # here reference = order number + assert_equal "000035-#{Time.current.strftime('%m-%y')}", cart.invoice.order_number + assert_equal "#{Time.current.strftime('%y%m')}017", cart.invoice.reference + + cart = Cart::FindOrCreateService.new(users(:user_2)).call(nil) + cart = Cart::AddItemService.new.call(cart, Product.find_by(slug: 'panneaux-de-mdf'), 1) + Checkout::PaymentService.new.payment(cart, @admin, nil) + assert_equal "000036-#{Time.current.strftime('%m-%y')}", cart.reference # here reference = order number + assert_equal "000036-#{Time.current.strftime('%m-%y')}", cart.invoice.order_number + assert_equal "#{Time.current.strftime('%y%m')}018", cart.invoice.reference + + invoice = sample_reservation_invoice(@acamus, @admin) + assert_equal "#{Time.current.strftime('%y%m')}019", invoice.reference + assert_equal "000037-#{Time.current.strftime('%m-%y')}", invoice.order_number + end +end diff --git a/test/services/reservation_limit_service_test.rb b/test/services/reservation_limit_service_test.rb new file mode 100644 index 000000000..5c9f432cf --- /dev/null +++ b/test/services/reservation_limit_service_test.rb @@ -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 diff --git a/test/test_helper.rb b/test/test_helper.rb index 3dc895be4..ec78bdec9 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -10,8 +10,9 @@ require 'rails/test_help' require 'vcr' require 'sidekiq/testing' require 'minitest/reporters' -require 'helpers/invoice_helper' require 'helpers/archive_helper' +require 'helpers/invoice_helper' +require 'helpers/payment_schedule_helper' require 'fileutils' VCR.configure do |config| @@ -31,8 +32,9 @@ Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(color: true)] class ActiveSupport::TestCase include ActionDispatch::TestProcess - include InvoiceHelper include ArchiveHelper + include InvoiceHelper + include PaymentScheduleHelper # Add more helper methods to be used by all tests here... ActiveRecord::Migration.check_pending! @@ -111,6 +113,11 @@ class ActiveSupport::TestCase assert_not_nil actual, msg assert_equal expected.to_date, actual.to_date, msg end + + def assert_datetimes_equal(expected, actual, msg = nil) + assert_not_nil actual, msg + assert_equal expected.iso8601, actual.iso8601, msg + end end class ActionDispatch::IntegrationTest diff --git a/test/vcr_cassettes/reservations_and_subscription_create_for_restricted_slot_success.yml b/test/vcr_cassettes/reservations_and_subscription_create_for_restricted_slot_success.yml index bcdd51f7f..ef8d646d4 100644 --- a/test/vcr_cassettes/reservations_and_subscription_create_for_restricted_slot_success.yml +++ b/test/vcr_cassettes/reservations_and_subscription_create_for_restricted_slot_success.yml @@ -5,7 +5,7 @@ http_interactions: uri: https://api.stripe.com/v1/payment_methods body: encoding: UTF-8 - string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2022&card[cvc]=314 + string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2024&card[cvc]=314 headers: User-Agent: - Stripe/v1 RubyBindings/5.29.0 @@ -13,12 +13,14 @@ http_interactions: - Bearer sk_test_testfaketestfaketestfake Content-Type: - application/x-www-form-urlencoded + X-Stripe-Client-Telemetry: + - '{"last_request_metrics":{"request_id":"req_MQd4Z7i8cW9FYF","request_duration_ms":535}}' Stripe-Version: - '2019-08-14' X-Stripe-Client-User-Agent: - - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin - MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 - PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld + (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}' Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -31,11 +33,11 @@ http_interactions: Server: - nginx Date: - - Mon, 13 Sep 2021 11:24:07 GMT + - Wed, 15 Mar 2023 11:51:50 GMT Content-Type: - application/json Content-Length: - - '934' + - '930' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -50,19 +52,23 @@ http_interactions: - '300' Cache-Control: - no-cache, no-store + Idempotency-Key: + - 8b19d06b-ed60-406d-8490-3d7062d47f67 + Original-Request: + - req_jrCbj5YQwrn3m7 Request-Id: - - req_tlTlxEJC4LyAQv + - req_jrCbj5YQwrn3m7 + Stripe-Should-Retry: + - 'false' Stripe-Version: - '2019-08-14' - X-Stripe-C-Cost: - - '6' Strict-Transport-Security: - - max-age=31556926; includeSubDomains; preload + - max-age=63072000; includeSubDomains; preload body: encoding: UTF-8 - string: | + string: |- { - "id": "pm_1JZDGd2sOmf47Nz9LCckU76B", + "id": "pm_1Mlsry2sOmf47Nz9g8twwVyn", "object": "payment_method", "billing_details": { "address": { @@ -86,7 +92,7 @@ http_interactions: }, "country": "US", "exp_month": 4, - "exp_year": 2022, + "exp_year": 2024, "fingerprint": "o52jybR7bnmNn6AT", "funding": "credit", "generated_from": null, @@ -102,20 +108,19 @@ http_interactions: }, "wallet": null }, - "created": 1631532247, + "created": 1678881110, "customer": null, "livemode": false, - "metadata": { - }, + "metadata": {}, "type": "card" } - recorded_at: Mon, 13 Sep 2021 11:24:07 GMT + recorded_at: Wed, 15 Mar 2023 11:51:50 GMT - request: method: post uri: https://api.stripe.com/v1/payment_intents body: encoding: UTF-8 - string: payment_method=pm_1JZDGd2sOmf47Nz9LCckU76B&amount=11500¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8Di1wjdVktv5kt + string: payment_method=pm_1Mlsry2sOmf47Nz9g8twwVyn&amount=11500¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8Di1wjdVktv5kt headers: User-Agent: - Stripe/v1 RubyBindings/5.29.0 @@ -124,13 +129,13 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-Telemetry: - - '{"last_request_metrics":{"request_id":"req_tlTlxEJC4LyAQv","request_duration_ms":663}}' + - '{"last_request_metrics":{"request_id":"req_jrCbj5YQwrn3m7","request_duration_ms":586}}' Stripe-Version: - '2019-08-14' X-Stripe-Client-User-Agent: - - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin - MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 - PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld + (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}' Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -143,11 +148,11 @@ http_interactions: Server: - nginx Date: - - Mon, 13 Sep 2021 11:24:09 GMT + - Wed, 15 Mar 2023 11:51:52 GMT Content-Type: - application/json Content-Length: - - '4263' + - '4522' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -162,25 +167,33 @@ http_interactions: - '300' Cache-Control: - no-cache, no-store + Idempotency-Key: + - bceb9ff3-545c-48c9-9f9b-4654dd50641b + Original-Request: + - req_2QEtRciNfNblB1 Request-Id: - - req_4hAutJ5WkAA9ps + - req_2QEtRciNfNblB1 + Stripe-Should-Retry: + - 'false' Stripe-Version: - '2019-08-14' - X-Stripe-C-Cost: - - '10' Strict-Transport-Security: - - max-age=31556926; includeSubDomains; preload + - max-age=63072000; includeSubDomains; preload body: encoding: UTF-8 - string: | + string: |- { - "id": "pi_3JZDGd2sOmf47Nz91tgWkK3L", + "id": "pi_3Mlsrz2sOmf47Nz90UCTYKFx", "object": "payment_intent", "amount": 11500, "amount_capturable": 0, + "amount_details": { + "tip": {} + }, "amount_received": 11500, "application": null, "application_fee_amount": null, + "automatic_payment_methods": null, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic", @@ -188,7 +201,7 @@ http_interactions: "object": "list", "data": [ { - "id": "ch_3JZDGd2sOmf47Nz91HsbEa5U", + "id": "ch_3Mlsrz2sOmf47Nz9004R5HME", "object": "charge", "amount": 11500, "amount_captured": 11500, @@ -196,7 +209,7 @@ http_interactions: "application": null, "application_fee": null, "application_fee_amount": null, - "balance_transaction": "txn_3JZDGd2sOmf47Nz91MEnox3F", + "balance_transaction": "txn_3Mlsrz2sOmf47Nz90SLqRLy1", "billing_details": { "address": { "city": null, @@ -212,34 +225,33 @@ http_interactions: }, "calculated_statement_descriptor": "Stripe", "captured": true, - "created": 1631532248, + "created": 1678881111, "currency": "usd", "customer": "cus_8Di1wjdVktv5kt", "description": null, "destination": null, "dispute": null, "disputed": false, + "failure_balance_transaction": null, "failure_code": null, "failure_message": null, - "fraud_details": { - }, + "fraud_details": {}, "invoice": null, "livemode": false, - "metadata": { - }, + "metadata": {}, "on_behalf_of": null, "order": null, "outcome": { "network_status": "approved_by_network", "reason": null, "risk_level": "normal", - "risk_score": 12, + "risk_score": 29, "seller_message": "Payment complete.", "type": "authorized" }, "paid": true, - "payment_intent": "pi_3JZDGd2sOmf47Nz91tgWkK3L", - "payment_method": "pm_1JZDGd2sOmf47Nz9LCckU76B", + "payment_intent": "pi_3Mlsrz2sOmf47Nz90UCTYKFx", + "payment_method": "pm_1Mlsry2sOmf47Nz9g8twwVyn", "payment_method_details": { "card": { "brand": "visa", @@ -250,11 +262,12 @@ http_interactions: }, "country": "US", "exp_month": 4, - "exp_year": 2022, + "exp_year": 2024, "fingerprint": "o52jybR7bnmNn6AT", "funding": "credit", "installments": null, "last4": "4242", + "mandate": null, "network": "visa", "three_d_secure": null, "wallet": null @@ -263,16 +276,14 @@ http_interactions: }, "receipt_email": null, "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDGd2sOmf47Nz91HsbEa5U/rcpt_KDeZXFrj8mqhXX4v6MKFwawzylc2kPA", + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xMDNyRTYyc09tZjQ3Tno5KNjixqAGMgZncKXPXn06LBY8nUVkDGBhMzC5bIzAPJWsR_S75nAIhMIDBqALhrU35BgWxXMZpcMss_ty", "refunded": false, "refunds": { "object": "list", - "data": [ - - ], + "data": [], "has_more": false, "total_count": 0, - "url": "/v1/charges/ch_3JZDGd2sOmf47Nz91HsbEa5U/refunds" + "url": "/v1/charges/ch_3Mlsrz2sOmf47Nz9004R5HME/refunds" }, "review": null, "shipping": null, @@ -287,25 +298,26 @@ http_interactions: ], "has_more": false, "total_count": 1, - "url": "/v1/charges?payment_intent=pi_3JZDGd2sOmf47Nz91tgWkK3L" + "url": "/v1/charges?payment_intent=pi_3Mlsrz2sOmf47Nz90UCTYKFx" }, - "client_secret": "pi_3JZDGd2sOmf47Nz91tgWkK3L_secret_DooP6j5YiNN0kzaXPdTGEeKeR", + "client_secret": "pi_3Mlsrz2sOmf47Nz90UCTYKFx_secret_af9V81CCyVZZ2J2sptymbRLES", "confirmation_method": "manual", - "created": 1631532247, + "created": 1678881111, "currency": "usd", "customer": "cus_8Di1wjdVktv5kt", "description": null, "invoice": null, "last_payment_error": null, + "latest_charge": "ch_3Mlsrz2sOmf47Nz9004R5HME", "livemode": false, - "metadata": { - }, + "metadata": {}, "next_action": null, "on_behalf_of": null, - "payment_method": "pm_1JZDGd2sOmf47Nz9LCckU76B", + "payment_method": "pm_1Mlsry2sOmf47Nz9g8twwVyn", "payment_method_options": { "card": { "installments": null, + "mandate_options": null, "network": null, "request_three_d_secure": "automatic" } @@ -313,6 +325,7 @@ http_interactions: "payment_method_types": [ "card" ], + "processing": null, "receipt_email": null, "review": null, "setup_future_usage": null, @@ -324,13 +337,13 @@ http_interactions: "transfer_data": null, "transfer_group": null } - recorded_at: Mon, 13 Sep 2021 11:24:09 GMT + recorded_at: Wed, 15 Mar 2023 11:51:52 GMT - request: method: post - uri: https://api.stripe.com/v1/payment_intents/pi_3JZDGd2sOmf47Nz91tgWkK3L + uri: https://api.stripe.com/v1/payment_intents/pi_3Mlsrz2sOmf47Nz90UCTYKFx body: encoding: UTF-8 - string: description=Invoice+reference%3A+2109001%2FVL + string: description=Invoice+reference%3A+2303007%2FVL headers: User-Agent: - Stripe/v1 RubyBindings/5.29.0 @@ -339,13 +352,13 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-Telemetry: - - '{"last_request_metrics":{"request_id":"req_4hAutJ5WkAA9ps","request_duration_ms":1725}}' + - '{"last_request_metrics":{"request_id":"req_2QEtRciNfNblB1","request_duration_ms":1482}}' Stripe-Version: - '2019-08-14' X-Stripe-Client-User-Agent: - - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin - MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 - PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld + (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}' Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -358,11 +371,11 @@ http_interactions: Server: - nginx Date: - - Mon, 13 Sep 2021 11:24:10 GMT + - Wed, 15 Mar 2023 11:51:52 GMT Content-Type: - application/json Content-Length: - - '4290' + - '4549' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -377,25 +390,33 @@ http_interactions: - '300' Cache-Control: - no-cache, no-store + Idempotency-Key: + - 51790c68-63e7-4b6e-96b1-4b6eef30b42b + Original-Request: + - req_Ms4raQ174jWmPK Request-Id: - - req_ysWG3JfyCp5xVD + - req_Ms4raQ174jWmPK + Stripe-Should-Retry: + - 'false' Stripe-Version: - '2019-08-14' - X-Stripe-C-Cost: - - '0' Strict-Transport-Security: - - max-age=31556926; includeSubDomains; preload + - max-age=63072000; includeSubDomains; preload body: encoding: UTF-8 - string: | + string: |- { - "id": "pi_3JZDGd2sOmf47Nz91tgWkK3L", + "id": "pi_3Mlsrz2sOmf47Nz90UCTYKFx", "object": "payment_intent", "amount": 11500, "amount_capturable": 0, + "amount_details": { + "tip": {} + }, "amount_received": 11500, "application": null, "application_fee_amount": null, + "automatic_payment_methods": null, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic", @@ -403,7 +424,7 @@ http_interactions: "object": "list", "data": [ { - "id": "ch_3JZDGd2sOmf47Nz91HsbEa5U", + "id": "ch_3Mlsrz2sOmf47Nz9004R5HME", "object": "charge", "amount": 11500, "amount_captured": 11500, @@ -411,7 +432,7 @@ http_interactions: "application": null, "application_fee": null, "application_fee_amount": null, - "balance_transaction": "txn_3JZDGd2sOmf47Nz91MEnox3F", + "balance_transaction": "txn_3Mlsrz2sOmf47Nz90SLqRLy1", "billing_details": { "address": { "city": null, @@ -427,34 +448,33 @@ http_interactions: }, "calculated_statement_descriptor": "Stripe", "captured": true, - "created": 1631532248, + "created": 1678881111, "currency": "usd", "customer": "cus_8Di1wjdVktv5kt", "description": null, "destination": null, "dispute": null, "disputed": false, + "failure_balance_transaction": null, "failure_code": null, "failure_message": null, - "fraud_details": { - }, + "fraud_details": {}, "invoice": null, "livemode": false, - "metadata": { - }, + "metadata": {}, "on_behalf_of": null, "order": null, "outcome": { "network_status": "approved_by_network", "reason": null, "risk_level": "normal", - "risk_score": 12, + "risk_score": 29, "seller_message": "Payment complete.", "type": "authorized" }, "paid": true, - "payment_intent": "pi_3JZDGd2sOmf47Nz91tgWkK3L", - "payment_method": "pm_1JZDGd2sOmf47Nz9LCckU76B", + "payment_intent": "pi_3Mlsrz2sOmf47Nz90UCTYKFx", + "payment_method": "pm_1Mlsry2sOmf47Nz9g8twwVyn", "payment_method_details": { "card": { "brand": "visa", @@ -465,11 +485,12 @@ http_interactions: }, "country": "US", "exp_month": 4, - "exp_year": 2022, + "exp_year": 2024, "fingerprint": "o52jybR7bnmNn6AT", "funding": "credit", "installments": null, "last4": "4242", + "mandate": null, "network": "visa", "three_d_secure": null, "wallet": null @@ -478,16 +499,14 @@ http_interactions: }, "receipt_email": null, "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDGd2sOmf47Nz91HsbEa5U/rcpt_KDeZXFrj8mqhXX4v6MKFwawzylc2kPA", + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xMDNyRTYyc09tZjQ3Tno5KNjixqAGMgbuQcWgG5c6LBa-ZSU_-3ZZaF7tt6uBsD2Rs604Am5ssLMpXbt0FvRIOVw-pVEwxmP_5ACg", "refunded": false, "refunds": { "object": "list", - "data": [ - - ], + "data": [], "has_more": false, "total_count": 0, - "url": "/v1/charges/ch_3JZDGd2sOmf47Nz91HsbEa5U/refunds" + "url": "/v1/charges/ch_3Mlsrz2sOmf47Nz9004R5HME/refunds" }, "review": null, "shipping": null, @@ -502,25 +521,26 @@ http_interactions: ], "has_more": false, "total_count": 1, - "url": "/v1/charges?payment_intent=pi_3JZDGd2sOmf47Nz91tgWkK3L" + "url": "/v1/charges?payment_intent=pi_3Mlsrz2sOmf47Nz90UCTYKFx" }, - "client_secret": "pi_3JZDGd2sOmf47Nz91tgWkK3L_secret_DooP6j5YiNN0kzaXPdTGEeKeR", + "client_secret": "pi_3Mlsrz2sOmf47Nz90UCTYKFx_secret_af9V81CCyVZZ2J2sptymbRLES", "confirmation_method": "manual", - "created": 1631532247, + "created": 1678881111, "currency": "usd", "customer": "cus_8Di1wjdVktv5kt", - "description": "Invoice reference: 2109001/VL", + "description": "Invoice reference: 2303007/VL", "invoice": null, "last_payment_error": null, + "latest_charge": "ch_3Mlsrz2sOmf47Nz9004R5HME", "livemode": false, - "metadata": { - }, + "metadata": {}, "next_action": null, "on_behalf_of": null, - "payment_method": "pm_1JZDGd2sOmf47Nz9LCckU76B", + "payment_method": "pm_1Mlsry2sOmf47Nz9g8twwVyn", "payment_method_options": { "card": { "installments": null, + "mandate_options": null, "network": null, "request_three_d_secure": "automatic" } @@ -528,6 +548,7 @@ http_interactions: "payment_method_types": [ "card" ], + "processing": null, "receipt_email": null, "review": null, "setup_future_usage": null, @@ -539,5 +560,5 @@ http_interactions: "transfer_data": null, "transfer_group": null } - recorded_at: Mon, 13 Sep 2021 11:24:10 GMT + recorded_at: Wed, 15 Mar 2023 11:51:53 GMT recorded_with: VCR 6.0.0 diff --git a/test/vcr_cassettes/reservations_create_for_restricted_slot_fails.yml b/test/vcr_cassettes/reservations_create_for_restricted_slot_fails.yml index 98aef3105..f3ae5c1c5 100644 --- a/test/vcr_cassettes/reservations_create_for_restricted_slot_fails.yml +++ b/test/vcr_cassettes/reservations_create_for_restricted_slot_fails.yml @@ -5,7 +5,7 @@ http_interactions: uri: https://api.stripe.com/v1/payment_methods body: encoding: UTF-8 - string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2022&card[cvc]=314 + string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2024&card[cvc]=314 headers: User-Agent: - Stripe/v1 RubyBindings/5.29.0 @@ -13,14 +13,12 @@ http_interactions: - Bearer sk_test_testfaketestfaketestfake Content-Type: - application/x-www-form-urlencoded - X-Stripe-Client-Telemetry: - - '{"last_request_metrics":{"request_id":"req_ysWG3JfyCp5xVD","request_duration_ms":520}}' Stripe-Version: - '2019-08-14' X-Stripe-Client-User-Agent: - - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin - MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 - PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld + (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}' Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -33,11 +31,11 @@ http_interactions: Server: - nginx Date: - - Mon, 13 Sep 2021 11:24:10 GMT + - Wed, 15 Mar 2023 11:51:20 GMT Content-Type: - application/json Content-Length: - - '934' + - '930' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -52,19 +50,23 @@ http_interactions: - '300' Cache-Control: - no-cache, no-store + Idempotency-Key: + - 6a183d91-3220-4ad4-913c-f169a75aa488 + Original-Request: + - req_LQKka6p7rniNKT Request-Id: - - req_7RNGSU2vySHdHz + - req_LQKka6p7rniNKT + Stripe-Should-Retry: + - 'false' Stripe-Version: - '2019-08-14' - X-Stripe-C-Cost: - - '6' Strict-Transport-Security: - - max-age=31556926; includeSubDomains; preload + - max-age=63072000; includeSubDomains; preload body: encoding: UTF-8 - string: | + string: |- { - "id": "pm_1JZDGg2sOmf47Nz9pfmMaPtb", + "id": "pm_1MlsrU2sOmf47Nz9voyfBlTb", "object": "payment_method", "billing_details": { "address": { @@ -88,7 +90,7 @@ http_interactions: }, "country": "US", "exp_month": 4, - "exp_year": 2022, + "exp_year": 2024, "fingerprint": "o52jybR7bnmNn6AT", "funding": "credit", "generated_from": null, @@ -104,227 +106,11 @@ http_interactions: }, "wallet": null }, - "created": 1631532250, + "created": 1678881080, "customer": null, "livemode": false, - "metadata": { - }, + "metadata": {}, "type": "card" } - recorded_at: Mon, 13 Sep 2021 11:24:10 GMT -- request: - method: post - uri: https://api.stripe.com/v1/payment_intents - body: - encoding: UTF-8 - string: payment_method=pm_1JZDGg2sOmf47Nz9pfmMaPtb&amount=4200¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8Di1wjdVktv5kt - headers: - User-Agent: - - Stripe/v1 RubyBindings/5.29.0 - Authorization: - - Bearer sk_test_testfaketestfaketestfake - Content-Type: - - application/x-www-form-urlencoded - X-Stripe-Client-Telemetry: - - '{"last_request_metrics":{"request_id":"req_7RNGSU2vySHdHz","request_duration_ms":628}}' - Stripe-Version: - - '2019-08-14' - X-Stripe-Client-User-Agent: - - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin - MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 - PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' - Accept-Encoding: - - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - Accept: - - "*/*" - response: - status: - code: 200 - message: OK - headers: - Server: - - nginx - Date: - - Mon, 13 Sep 2021 11:24:12 GMT - Content-Type: - - application/json - Content-Length: - - '4258' - Connection: - - keep-alive - Access-Control-Allow-Credentials: - - 'true' - Access-Control-Allow-Methods: - - GET, POST, HEAD, OPTIONS, DELETE - Access-Control-Allow-Origin: - - "*" - Access-Control-Expose-Headers: - - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required - Access-Control-Max-Age: - - '300' - Cache-Control: - - no-cache, no-store - Request-Id: - - req_NgGOJxEFd8THv8 - Stripe-Version: - - '2019-08-14' - X-Stripe-C-Cost: - - '10' - Strict-Transport-Security: - - max-age=31556926; includeSubDomains; preload - body: - encoding: UTF-8 - string: | - { - "id": "pi_3JZDGh2sOmf47Nz91FT4yZ2t", - "object": "payment_intent", - "amount": 4200, - "amount_capturable": 0, - "amount_received": 4200, - "application": null, - "application_fee_amount": null, - "canceled_at": null, - "cancellation_reason": null, - "capture_method": "automatic", - "charges": { - "object": "list", - "data": [ - { - "id": "ch_3JZDGh2sOmf47Nz91FoAsBFe", - "object": "charge", - "amount": 4200, - "amount_captured": 4200, - "amount_refunded": 0, - "application": null, - "application_fee": null, - "application_fee_amount": null, - "balance_transaction": "txn_3JZDGh2sOmf47Nz91yuRvukb", - "billing_details": { - "address": { - "city": null, - "country": null, - "line1": null, - "line2": null, - "postal_code": null, - "state": null - }, - "email": null, - "name": null, - "phone": null - }, - "calculated_statement_descriptor": "Stripe", - "captured": true, - "created": 1631532251, - "currency": "usd", - "customer": "cus_8Di1wjdVktv5kt", - "description": null, - "destination": null, - "dispute": null, - "disputed": false, - "failure_code": null, - "failure_message": null, - "fraud_details": { - }, - "invoice": null, - "livemode": false, - "metadata": { - }, - "on_behalf_of": null, - "order": null, - "outcome": { - "network_status": "approved_by_network", - "reason": null, - "risk_level": "normal", - "risk_score": 2, - "seller_message": "Payment complete.", - "type": "authorized" - }, - "paid": true, - "payment_intent": "pi_3JZDGh2sOmf47Nz91FT4yZ2t", - "payment_method": "pm_1JZDGg2sOmf47Nz9pfmMaPtb", - "payment_method_details": { - "card": { - "brand": "visa", - "checks": { - "address_line1_check": null, - "address_postal_code_check": null, - "cvc_check": "pass" - }, - "country": "US", - "exp_month": 4, - "exp_year": 2022, - "fingerprint": "o52jybR7bnmNn6AT", - "funding": "credit", - "installments": null, - "last4": "4242", - "network": "visa", - "three_d_secure": null, - "wallet": null - }, - "type": "card" - }, - "receipt_email": null, - "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDGh2sOmf47Nz91FoAsBFe/rcpt_KDeZ4pRoBzCyvhebh2wUzvr5fmdZdtD", - "refunded": false, - "refunds": { - "object": "list", - "data": [ - - ], - "has_more": false, - "total_count": 0, - "url": "/v1/charges/ch_3JZDGh2sOmf47Nz91FoAsBFe/refunds" - }, - "review": null, - "shipping": null, - "source": null, - "source_transfer": null, - "statement_descriptor": null, - "statement_descriptor_suffix": null, - "status": "succeeded", - "transfer_data": null, - "transfer_group": null - } - ], - "has_more": false, - "total_count": 1, - "url": "/v1/charges?payment_intent=pi_3JZDGh2sOmf47Nz91FT4yZ2t" - }, - "client_secret": "pi_3JZDGh2sOmf47Nz91FT4yZ2t_secret_F3QBmBEtZjcaKblNLMUnLO6hD", - "confirmation_method": "manual", - "created": 1631532251, - "currency": "usd", - "customer": "cus_8Di1wjdVktv5kt", - "description": null, - "invoice": null, - "last_payment_error": null, - "livemode": false, - "metadata": { - }, - "next_action": null, - "on_behalf_of": null, - "payment_method": "pm_1JZDGg2sOmf47Nz9pfmMaPtb", - "payment_method_options": { - "card": { - "installments": null, - "network": null, - "request_three_d_secure": "automatic" - } - }, - "payment_method_types": [ - "card" - ], - "receipt_email": null, - "review": null, - "setup_future_usage": null, - "shipping": null, - "source": null, - "statement_descriptor": null, - "statement_descriptor_suffix": null, - "status": "succeeded", - "transfer_data": null, - "transfer_group": null - } - recorded_at: Mon, 13 Sep 2021 11:24:12 GMT + recorded_at: Wed, 15 Mar 2023 11:51:20 GMT recorded_with: VCR 6.0.0 diff --git a/test/vcr_cassettes/reservations_create_for_restricted_slot_success.yml b/test/vcr_cassettes/reservations_create_for_restricted_slot_success.yml index a4a9e06fc..280cfe25c 100644 --- a/test/vcr_cassettes/reservations_create_for_restricted_slot_success.yml +++ b/test/vcr_cassettes/reservations_create_for_restricted_slot_success.yml @@ -5,7 +5,7 @@ http_interactions: uri: https://api.stripe.com/v1/payment_methods body: encoding: UTF-8 - string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2022&card[cvc]=314 + string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2024&card[cvc]=314 headers: User-Agent: - Stripe/v1 RubyBindings/5.29.0 @@ -13,14 +13,12 @@ http_interactions: - Bearer sk_test_testfaketestfaketestfake Content-Type: - application/x-www-form-urlencoded - X-Stripe-Client-Telemetry: - - '{"last_request_metrics":{"request_id":"req_NgGOJxEFd8THv8","request_duration_ms":1772}}' Stripe-Version: - '2019-08-14' X-Stripe-Client-User-Agent: - - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin - MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 - PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld + (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}' Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -33,11 +31,11 @@ http_interactions: Server: - nginx Date: - - Mon, 13 Sep 2021 11:24:13 GMT + - Wed, 15 Mar 2023 11:51:46 GMT Content-Type: - application/json Content-Length: - - '934' + - '930' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -52,19 +50,23 @@ http_interactions: - '300' Cache-Control: - no-cache, no-store + Idempotency-Key: + - 4a6e0351-64bd-4ede-a018-7c03e320c3f5 + Original-Request: + - req_njTGdMZNpa6wpG Request-Id: - - req_px5zsAdlzgwwSe + - req_njTGdMZNpa6wpG + Stripe-Should-Retry: + - 'false' Stripe-Version: - '2019-08-14' - X-Stripe-C-Cost: - - '6' Strict-Transport-Security: - - max-age=31556926; includeSubDomains; preload + - max-age=63072000; includeSubDomains; preload body: encoding: UTF-8 - string: | + string: |- { - "id": "pm_1JZDGj2sOmf47Nz9S8jhZkFt", + "id": "pm_1Mlsru2sOmf47Nz9s3VNHXYt", "object": "payment_method", "billing_details": { "address": { @@ -88,7 +90,7 @@ http_interactions: }, "country": "US", "exp_month": 4, - "exp_year": 2022, + "exp_year": 2024, "fingerprint": "o52jybR7bnmNn6AT", "funding": "credit", "generated_from": null, @@ -104,20 +106,19 @@ http_interactions: }, "wallet": null }, - "created": 1631532253, + "created": 1678881106, "customer": null, "livemode": false, - "metadata": { - }, + "metadata": {}, "type": "card" } - recorded_at: Mon, 13 Sep 2021 11:24:13 GMT + recorded_at: Wed, 15 Mar 2023 11:51:46 GMT - request: method: post uri: https://api.stripe.com/v1/payment_intents body: encoding: UTF-8 - string: payment_method=pm_1JZDGj2sOmf47Nz9S8jhZkFt&amount=1500¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8CzHcwBJtlA3IL + string: payment_method=pm_1Mlsru2sOmf47Nz9s3VNHXYt&amount=1500¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8CzHcwBJtlA3IL headers: User-Agent: - Stripe/v1 RubyBindings/5.29.0 @@ -126,13 +127,13 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-Telemetry: - - '{"last_request_metrics":{"request_id":"req_px5zsAdlzgwwSe","request_duration_ms":619}}' + - '{"last_request_metrics":{"request_id":"req_njTGdMZNpa6wpG","request_duration_ms":581}}' Stripe-Version: - '2019-08-14' X-Stripe-Client-User-Agent: - - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin - MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 - PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld + (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}' Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -145,11 +146,11 @@ http_interactions: Server: - nginx Date: - - Mon, 13 Sep 2021 11:24:15 GMT + - Wed, 15 Mar 2023 11:51:48 GMT Content-Type: - application/json Content-Length: - - '4259' + - '4517' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -164,25 +165,33 @@ http_interactions: - '300' Cache-Control: - no-cache, no-store + Idempotency-Key: + - f50a36a7-97b5-4d67-9c77-43b5cbddccbe + Original-Request: + - req_EocoIWBH6tucS4 Request-Id: - - req_EMDO1Z1Uux0kJb + - req_EocoIWBH6tucS4 + Stripe-Should-Retry: + - 'false' Stripe-Version: - '2019-08-14' - X-Stripe-C-Cost: - - '10' Strict-Transport-Security: - - max-age=31556926; includeSubDomains; preload + - max-age=63072000; includeSubDomains; preload body: encoding: UTF-8 - string: | + string: |- { - "id": "pi_3JZDGj2sOmf47Nz90n2aOsuM", + "id": "pi_3Mlsrv2sOmf47Nz91hRe49rq", "object": "payment_intent", "amount": 1500, "amount_capturable": 0, + "amount_details": { + "tip": {} + }, "amount_received": 1500, "application": null, "application_fee_amount": null, + "automatic_payment_methods": null, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic", @@ -190,7 +199,7 @@ http_interactions: "object": "list", "data": [ { - "id": "ch_3JZDGj2sOmf47Nz90qQzwGqL", + "id": "ch_3Mlsrv2sOmf47Nz91760imzX", "object": "charge", "amount": 1500, "amount_captured": 1500, @@ -198,7 +207,7 @@ http_interactions: "application": null, "application_fee": null, "application_fee_amount": null, - "balance_transaction": "txn_3JZDGj2sOmf47Nz90fOwfgv5", + "balance_transaction": "txn_3Mlsrv2sOmf47Nz915VOM7IM", "billing_details": { "address": { "city": null, @@ -214,34 +223,33 @@ http_interactions: }, "calculated_statement_descriptor": "Stripe", "captured": true, - "created": 1631532254, + "created": 1678881107, "currency": "usd", "customer": "cus_8CzHcwBJtlA3IL", "description": null, "destination": null, "dispute": null, "disputed": false, + "failure_balance_transaction": null, "failure_code": null, "failure_message": null, - "fraud_details": { - }, + "fraud_details": {}, "invoice": null, "livemode": false, - "metadata": { - }, + "metadata": {}, "on_behalf_of": null, "order": null, "outcome": { "network_status": "approved_by_network", "reason": null, "risk_level": "normal", - "risk_score": 22, + "risk_score": 7, "seller_message": "Payment complete.", "type": "authorized" }, "paid": true, - "payment_intent": "pi_3JZDGj2sOmf47Nz90n2aOsuM", - "payment_method": "pm_1JZDGj2sOmf47Nz9S8jhZkFt", + "payment_intent": "pi_3Mlsrv2sOmf47Nz91hRe49rq", + "payment_method": "pm_1Mlsru2sOmf47Nz9s3VNHXYt", "payment_method_details": { "card": { "brand": "visa", @@ -252,11 +260,12 @@ http_interactions: }, "country": "US", "exp_month": 4, - "exp_year": 2022, + "exp_year": 2024, "fingerprint": "o52jybR7bnmNn6AT", "funding": "credit", "installments": null, "last4": "4242", + "mandate": null, "network": "visa", "three_d_secure": null, "wallet": null @@ -265,16 +274,14 @@ http_interactions: }, "receipt_email": null, "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDGj2sOmf47Nz90qQzwGqL/rcpt_KDeZMofbRKdRjLxxUw4LZO1LIDJP1sR", + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xMDNyRTYyc09tZjQ3Tno5KNTixqAGMgYuj2m72sQ6LBbKXya9pk9SHbdsAtPEkvHFS51bMvmjrLtHgTEs1uQ8YSkOpfxQRpKaC0PP", "refunded": false, "refunds": { "object": "list", - "data": [ - - ], + "data": [], "has_more": false, "total_count": 0, - "url": "/v1/charges/ch_3JZDGj2sOmf47Nz90qQzwGqL/refunds" + "url": "/v1/charges/ch_3Mlsrv2sOmf47Nz91760imzX/refunds" }, "review": null, "shipping": null, @@ -289,25 +296,26 @@ http_interactions: ], "has_more": false, "total_count": 1, - "url": "/v1/charges?payment_intent=pi_3JZDGj2sOmf47Nz90n2aOsuM" + "url": "/v1/charges?payment_intent=pi_3Mlsrv2sOmf47Nz91hRe49rq" }, - "client_secret": "pi_3JZDGj2sOmf47Nz90n2aOsuM_secret_WR5cdTATWgOShT8ZHInabONRL", + "client_secret": "pi_3Mlsrv2sOmf47Nz91hRe49rq_secret_gLvEqjCexLXGNWDLb7g2NpnfB", "confirmation_method": "manual", - "created": 1631532253, + "created": 1678881107, "currency": "usd", "customer": "cus_8CzHcwBJtlA3IL", "description": null, "invoice": null, "last_payment_error": null, + "latest_charge": "ch_3Mlsrv2sOmf47Nz91760imzX", "livemode": false, - "metadata": { - }, + "metadata": {}, "next_action": null, "on_behalf_of": null, - "payment_method": "pm_1JZDGj2sOmf47Nz9S8jhZkFt", + "payment_method": "pm_1Mlsru2sOmf47Nz9s3VNHXYt", "payment_method_options": { "card": { "installments": null, + "mandate_options": null, "network": null, "request_three_d_secure": "automatic" } @@ -315,6 +323,7 @@ http_interactions: "payment_method_types": [ "card" ], + "processing": null, "receipt_email": null, "review": null, "setup_future_usage": null, @@ -326,13 +335,13 @@ http_interactions: "transfer_data": null, "transfer_group": null } - recorded_at: Mon, 13 Sep 2021 11:24:15 GMT + recorded_at: Wed, 15 Mar 2023 11:51:48 GMT - request: method: post - uri: https://api.stripe.com/v1/payment_intents/pi_3JZDGj2sOmf47Nz90n2aOsuM + uri: https://api.stripe.com/v1/payment_intents/pi_3Mlsrv2sOmf47Nz91hRe49rq body: encoding: UTF-8 - string: description=Invoice+reference%3A+2109001%2FVL + string: description=Invoice+reference%3A+2303007%2FVL headers: User-Agent: - Stripe/v1 RubyBindings/5.29.0 @@ -341,13 +350,13 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-Telemetry: - - '{"last_request_metrics":{"request_id":"req_EMDO1Z1Uux0kJb","request_duration_ms":1528}}' + - '{"last_request_metrics":{"request_id":"req_EocoIWBH6tucS4","request_duration_ms":1593}}' Stripe-Version: - '2019-08-14' X-Stripe-Client-User-Agent: - - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin - MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 - PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 6.2.5-arch1-1 (linux@archlinux) (gcc (GCC) 12.2.1 20230201, GNU ld + (GNU Binutils) 2.40) #1 SMP PREEMPT_DYNAMIC Sat, 11 Mar 2023 14:28:13 +0000","hostname":"Sylvain-desktop"}' Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -360,11 +369,11 @@ http_interactions: Server: - nginx Date: - - Mon, 13 Sep 2021 11:24:15 GMT + - Wed, 15 Mar 2023 11:51:49 GMT Content-Type: - application/json Content-Length: - - '4286' + - '4544' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -379,25 +388,33 @@ http_interactions: - '300' Cache-Control: - no-cache, no-store + Idempotency-Key: + - 75c78d8d-9df1-4bb7-af1b-7de3aeaa7056 + Original-Request: + - req_MQd4Z7i8cW9FYF Request-Id: - - req_Xmih0ndHQjzde4 + - req_MQd4Z7i8cW9FYF + Stripe-Should-Retry: + - 'false' Stripe-Version: - '2019-08-14' - X-Stripe-C-Cost: - - '0' Strict-Transport-Security: - - max-age=31556926; includeSubDomains; preload + - max-age=63072000; includeSubDomains; preload body: encoding: UTF-8 - string: | + string: |- { - "id": "pi_3JZDGj2sOmf47Nz90n2aOsuM", + "id": "pi_3Mlsrv2sOmf47Nz91hRe49rq", "object": "payment_intent", "amount": 1500, "amount_capturable": 0, + "amount_details": { + "tip": {} + }, "amount_received": 1500, "application": null, "application_fee_amount": null, + "automatic_payment_methods": null, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic", @@ -405,7 +422,7 @@ http_interactions: "object": "list", "data": [ { - "id": "ch_3JZDGj2sOmf47Nz90qQzwGqL", + "id": "ch_3Mlsrv2sOmf47Nz91760imzX", "object": "charge", "amount": 1500, "amount_captured": 1500, @@ -413,7 +430,7 @@ http_interactions: "application": null, "application_fee": null, "application_fee_amount": null, - "balance_transaction": "txn_3JZDGj2sOmf47Nz90fOwfgv5", + "balance_transaction": "txn_3Mlsrv2sOmf47Nz915VOM7IM", "billing_details": { "address": { "city": null, @@ -429,34 +446,33 @@ http_interactions: }, "calculated_statement_descriptor": "Stripe", "captured": true, - "created": 1631532254, + "created": 1678881107, "currency": "usd", "customer": "cus_8CzHcwBJtlA3IL", "description": null, "destination": null, "dispute": null, "disputed": false, + "failure_balance_transaction": null, "failure_code": null, "failure_message": null, - "fraud_details": { - }, + "fraud_details": {}, "invoice": null, "livemode": false, - "metadata": { - }, + "metadata": {}, "on_behalf_of": null, "order": null, "outcome": { "network_status": "approved_by_network", "reason": null, "risk_level": "normal", - "risk_score": 22, + "risk_score": 7, "seller_message": "Payment complete.", "type": "authorized" }, "paid": true, - "payment_intent": "pi_3JZDGj2sOmf47Nz90n2aOsuM", - "payment_method": "pm_1JZDGj2sOmf47Nz9S8jhZkFt", + "payment_intent": "pi_3Mlsrv2sOmf47Nz91hRe49rq", + "payment_method": "pm_1Mlsru2sOmf47Nz9s3VNHXYt", "payment_method_details": { "card": { "brand": "visa", @@ -467,11 +483,12 @@ http_interactions: }, "country": "US", "exp_month": 4, - "exp_year": 2022, + "exp_year": 2024, "fingerprint": "o52jybR7bnmNn6AT", "funding": "credit", "installments": null, "last4": "4242", + "mandate": null, "network": "visa", "three_d_secure": null, "wallet": null @@ -480,16 +497,14 @@ http_interactions: }, "receipt_email": null, "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDGj2sOmf47Nz90qQzwGqL/rcpt_KDeZMofbRKdRjLxxUw4LZO1LIDJP1sR", + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xMDNyRTYyc09tZjQ3Tno5KNXixqAGMgYPhCqQTfg6LBaYDHIkkSjReWHiH0dGP8uhn2RZUgWlfTBRya6YV380PXzkqIYppHOEGGj9", "refunded": false, "refunds": { "object": "list", - "data": [ - - ], + "data": [], "has_more": false, "total_count": 0, - "url": "/v1/charges/ch_3JZDGj2sOmf47Nz90qQzwGqL/refunds" + "url": "/v1/charges/ch_3Mlsrv2sOmf47Nz91760imzX/refunds" }, "review": null, "shipping": null, @@ -504,25 +519,26 @@ http_interactions: ], "has_more": false, "total_count": 1, - "url": "/v1/charges?payment_intent=pi_3JZDGj2sOmf47Nz90n2aOsuM" + "url": "/v1/charges?payment_intent=pi_3Mlsrv2sOmf47Nz91hRe49rq" }, - "client_secret": "pi_3JZDGj2sOmf47Nz90n2aOsuM_secret_WR5cdTATWgOShT8ZHInabONRL", + "client_secret": "pi_3Mlsrv2sOmf47Nz91hRe49rq_secret_gLvEqjCexLXGNWDLb7g2NpnfB", "confirmation_method": "manual", - "created": 1631532253, + "created": 1678881107, "currency": "usd", "customer": "cus_8CzHcwBJtlA3IL", - "description": "Invoice reference: 2109001/VL", + "description": "Invoice reference: 2303007/VL", "invoice": null, "last_payment_error": null, + "latest_charge": "ch_3Mlsrv2sOmf47Nz91760imzX", "livemode": false, - "metadata": { - }, + "metadata": {}, "next_action": null, "on_behalf_of": null, - "payment_method": "pm_1JZDGj2sOmf47Nz9S8jhZkFt", + "payment_method": "pm_1Mlsru2sOmf47Nz9s3VNHXYt", "payment_method_options": { "card": { "installments": null, + "mandate_options": null, "network": null, "request_three_d_secure": "automatic" } @@ -530,6 +546,7 @@ http_interactions: "payment_method_types": [ "card" ], + "processing": null, "receipt_email": null, "review": null, "setup_future_usage": null, @@ -541,5 +558,5 @@ http_interactions: "transfer_data": null, "transfer_group": null } - recorded_at: Mon, 13 Sep 2021 11:24:15 GMT + recorded_at: Wed, 15 Mar 2023 11:51:49 GMT recorded_with: VCR 6.0.0 diff --git a/yarn.lock b/yarn.lock index 2afdac520..e02cd67c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3917,15 +3917,10 @@ acorn-walk@^8.0.2: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^8.1.0, acorn@^8.8.0, acorn@^8.8.1: - version "8.8.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" - integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== - -acorn@^8.4.1, acorn@^8.5.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" - integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== +acorn@^8.1.0, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.1: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== adjust-sourcemap-loader@^4.0.0: version "4.0.0" @@ -4550,40 +4545,7 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.17.5, browserslist@^4.19.1: - version "4.20.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.0.tgz#35951e3541078c125d36df76056e94738a52ebe9" - integrity sha512-bnpOoa+DownbciXj0jVGENf8VYQnE2LNWomhYuCsMmmx9Jd9lwq0WXODuwpSsp8AVdKM2/HorrzxAfbKvWTByQ== - dependencies: - caniuse-lite "^1.0.30001313" - electron-to-chromium "^1.4.76" - escalade "^3.1.1" - node-releases "^2.0.2" - picocolors "^1.0.0" - -browserslist@^4.16.6: - version "4.16.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" - integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== - dependencies: - caniuse-lite "^1.0.30001219" - colorette "^1.2.2" - electron-to-chromium "^1.3.723" - escalade "^3.1.1" - node-releases "^1.1.71" - -browserslist@^4.20.2, browserslist@^4.20.3: - version "4.20.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf" - integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg== - dependencies: - caniuse-lite "^1.0.30001332" - electron-to-chromium "^1.4.118" - escalade "^3.1.1" - node-releases "^2.0.3" - picocolors "^1.0.0" - -browserslist@^4.21.3, browserslist@^4.21.4: +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.17.5, browserslist@^4.19.1, browserslist@^4.20.2, browserslist@^4.20.3, browserslist@^4.21.3, browserslist@^4.21.4: version "4.21.4" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== @@ -4664,15 +4626,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001313, caniuse-lite@^1.0.30001332: - version "1.0.30001397" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001397.tgz" - integrity sha512-SW9N2TbCdLf0eiNDRrrQXx2sOkaakNZbCjgNpPyMJJbiOrU5QzMIrXOVMRM1myBXTD5iTkdrtU/EguCrBocHlA== - -caniuse-lite@^1.0.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== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001400: + version "1.0.30001460" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz" + integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ== chalk@4.1.1: version "4.1.1" @@ -4853,11 +4810,6 @@ colord@^2.9.1: resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1" integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ== -colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== - colorette@^2.0.10, colorette@^2.0.14: version "2.0.16" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" @@ -5451,26 +5403,11 @@ elasticsearch-browser@3.1: resolved "https://registry.yarnpkg.com/elasticsearch-browser/-/elasticsearch-browser-3.1.4.tgz#7e7db76e96e02b8a69c7659a997df421d4035565" integrity sha1-fn23bpbgK4ppx2WamX30IdQDVWU= -electron-to-chromium@^1.3.723: - version "1.3.752" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz#0728587f1b9b970ec9ffad932496429aef750d09" - integrity sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A== - -electron-to-chromium@^1.4.118: - version "1.4.129" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.129.tgz#c675793885721beefff99da50f57c6525c2cd238" - integrity sha512-GgtN6bsDtHdtXJtlMYZWGB/uOyjZWjmRDumXTas7dGBaB9zUyCjzHet1DY2KhyHN8R0GLbzZWqm4efeddqqyRQ== - electron-to-chromium@^1.4.251: version "1.4.284" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== -electron-to-chromium@^1.4.76: - version "1.4.78" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.78.tgz#7a1cf853efafde2c4cf6e86facf3e5792d3541a5" - integrity sha512-o61+D/Lx7j/E0LIin/efOqeHpXhwi1TaQco9vUcRmr91m25SfZY6L5hWJDv/r+6kNjboFKgBw1LbfM0lbhuK6Q== - emittery@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" @@ -5491,10 +5428,10 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -enhanced-resolve@^5.9.2: - version "5.9.2" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz#0224dcd6a43389ebfb2d55efee517e5466772dd9" - integrity sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA== +enhanced-resolve@^5.10.0: + version "5.12.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" + integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -6384,12 +6321,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== - -graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.9" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== @@ -7612,12 +7544,7 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -json-parse-better-errors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-parse-even-better-errors@^2.3.0: +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== @@ -7931,42 +7858,23 @@ micromatch@^4.0.2, micromatch@^4.0.4: braces "^3.0.1" picomatch "^2.2.3" -mime-db@1.48.0, "mime-db@>= 1.43.0 < 2": - version "1.48.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" - integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== - -mime-db@1.51.0: - version "1.51.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" - integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== - mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: +"mime-db@>= 1.43.0 < 2": + version "1.48.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" + integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" -mime-types@^2.1.27, mime-types@^2.1.31: - version "2.1.34" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" - integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== - dependencies: - mime-db "1.51.0" - -mime-types@~2.1.17: - version "2.1.31" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" - integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg== - dependencies: - mime-db "1.48.0" - mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -8147,21 +8055,6 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== -node-releases@^1.1.71: - version "1.1.73" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20" - integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg== - -node-releases@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" - integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== - -node-releases@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" - integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ== - node-releases@^2.0.6: version "2.0.7" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.7.tgz#593edbc7c22860ee4d32d3933cfebdfab0c0e0e5" @@ -8242,6 +8135,7 @@ object-keys@^1.0.12, object-keys@^1.1.1: integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object-to-formdata-tz@4.4.3, "object-to-formdata@npm:object-to-formdata-tz@4.4.3": + name object-to-formdata version "4.4.3" resolved "https://registry.yarnpkg.com/object-to-formdata-tz/-/object-to-formdata-tz-4.4.3.tgz#3059059d0f02ce90c7fdd9d83f491e8af34707ae" integrity sha512-3XK2hDLCUAfpwatU6Jr3WzzF3ncmzScXPUiIOWgXdYwnxijCojqH41w3DdHRLoPs3MgUHzHBAtLVOFmSlaDWlQ== @@ -9066,10 +8960,10 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" -react-hook-form@^7.30.0: - version "7.30.0" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.30.0.tgz#c9e2fd54d3627e43bd94bf38ef549df2e80c1371" - integrity sha512-DzjiM6o2vtDGNMB9I4yCqW8J21P314SboNG1O0obROkbg7KVS0I7bMtwSdKyapnCPjHgnxc3L7E5PEdISeEUcQ== +react-hook-form@~7.31.3: + version "7.31.3" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.31.3.tgz#b61bafb9a7435f91695351a7a9f714d8c4df0121" + integrity sha512-NVZdCWViIWXXXlQ3jxVQH0NuNfwPf8A/0KvuCxrM9qxtP1qYosfR2ZudarziFrVOC7eTUbWbm1T4OyYCwv9oSQ== react-i18next@^11.15.6: version "11.15.6" @@ -10461,10 +10355,10 @@ warning@^4.0.3: dependencies: loose-envify "^1.0.0" -watchpack@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25" - integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA== +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -10592,34 +10486,34 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@5.72.0: - version "5.72.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.72.0.tgz#f8bc40d9c6bb489a4b7a8a685101d6022b8b6e28" - integrity sha512-qmSmbspI0Qo5ld49htys8GY9XhS9CGqFoHTsOVAnjBdg0Zn79y135R+k4IR4rKK6+eKaabMhJwiVB7xw0SJu5w== +webpack@5.76.0: + version "5.76.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.0.tgz#f9fb9fb8c4a7dbdcd0d56a98e56b8a942ee2692c" + integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^0.0.51" "@webassemblyjs/ast" "1.11.1" "@webassemblyjs/wasm-edit" "1.11.1" "@webassemblyjs/wasm-parser" "1.11.1" - acorn "^8.4.1" + acorn "^8.7.1" acorn-import-assertions "^1.7.6" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.9.2" + enhanced-resolve "^5.10.0" es-module-lexer "^0.9.0" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" graceful-fs "^4.2.9" - json-parse-better-errors "^1.0.2" + json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" schema-utils "^3.1.0" tapable "^2.1.1" terser-webpack-plugin "^5.1.3" - watchpack "^2.3.1" + watchpack "^2.4.0" webpack-sources "^3.2.3" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: