mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
Merge branch 'dev' for release 5.8.2
This commit is contained in:
commit
f79f951fa7
18
CHANGELOG.md
18
CHANGELOG.md
@ -1,5 +1,23 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
## v5.8.2 2023 March 13
|
||||
|
||||
- Improved upgrade script
|
||||
- Keep usage history of prepaid packs
|
||||
- OpenAPI reservation endpoint can be filtered by date
|
||||
- OpenAPI users endpoint now returns the ID of the InvoicingProfile
|
||||
- Fix a bug: URL validation regex was wrong
|
||||
- Fix a bug: privileged users cannot order free carts for themselves in the store
|
||||
- Fix a bug: unable to select a new machine for an existing category
|
||||
- Fix a bug: wrong counting of minutes used when using a prepaid pack
|
||||
- Fix a bug: empty advanced accounting code is not defaulted to the general setting
|
||||
- Fix a bug: invalid style in accounting codes settings
|
||||
- Fix a bug: wrong namespace for task cart_operator
|
||||
- Fix a security issue: updated rack to 2.2.6.3 to fix [CVE-2023-27530](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-27530)
|
||||
- [TODO DEPLOY] `rails fablab:fix:cart_operator`
|
||||
- [TODO DEPLOY] `rails fablab:setup:build_accounting_lines`
|
||||
- [TODO DEPLOY] `rails fablab:fix:pack_minutes_used`
|
||||
|
||||
## v5.8.1 2023 March 03
|
||||
|
||||
- Fix a bug: unable to reserve an event
|
||||
|
@ -299,7 +299,7 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.1)
|
||||
rack (2.2.6.2)
|
||||
rack (2.2.6.3)
|
||||
rack-oauth2 (1.19.0)
|
||||
activesupport
|
||||
attr_required
|
||||
|
@ -6,6 +6,9 @@ class API::UserPacksController < API::ApiController
|
||||
|
||||
def index
|
||||
@user_packs = PrepaidPackService.user_packs(user, item)
|
||||
|
||||
@history = params[:history] == 'true'
|
||||
@user_packs = @user_packs.includes(:prepaid_pack_reservations) if @history
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -1,9 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'concerns/accountings_filters_concern'
|
||||
|
||||
# authorized 3rd party softwares can fetch the accounting lines through the OpenAPI
|
||||
class OpenAPI::V1::AccountingController < OpenAPI::V1::BaseController
|
||||
extend OpenAPI::ApiDoc
|
||||
include Rails::Pagination
|
||||
include AccountingsFiltersConcern
|
||||
expose_doc
|
||||
|
||||
def index
|
||||
@ -16,10 +19,10 @@ class OpenAPI::V1::AccountingController < OpenAPI::V1::BaseController
|
||||
@lines = AccountingLine.order(date: :desc)
|
||||
.includes(:invoicing_profile, invoice: :payment_gateway_object)
|
||||
|
||||
@lines = @lines.where('date >= ?', Time.zone.parse(params[:after])) if params[:after].present?
|
||||
@lines = @lines.where('date <= ?', Time.zone.parse(params[:before])) if params[:before].present?
|
||||
@lines = @lines.where(invoice_id: may_array(params[:invoice_id])) if params[:invoice_id].present?
|
||||
@lines = @lines.where(line_type: may_array(params[:type])) if params[:type].present?
|
||||
@lines = filter_by_after(@lines, params)
|
||||
@lines = filter_by_before(@lines, params)
|
||||
@lines = filter_by_invoice(@lines, params)
|
||||
@lines = filter_by_line_type(@lines, params)
|
||||
|
||||
@lines = @lines.page(page).per(per_page)
|
||||
paginate @lines, per_page: per_page
|
||||
|
@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Filter the list of accounting lines by the given parameters
|
||||
module AccountingsFiltersConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# @param lines [ActiveRecord::Relation<AccountingLine>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_after(lines, filters)
|
||||
return lines if filters[:after].blank?
|
||||
|
||||
lines.where('date >= ?', Time.zone.parse(filters[:after]))
|
||||
end
|
||||
|
||||
# @param lines [ActiveRecord::Relation<AccountingLine>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_before(lines, filters)
|
||||
return lines if filters[:before].blank?
|
||||
|
||||
lines.where('date <= ?', Time.zone.parse(filters[:before]))
|
||||
end
|
||||
|
||||
# @param lines [ActiveRecord::Relation<AccountingLine>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_invoice(lines, filters)
|
||||
return lines if filters[:invoice_id].blank?
|
||||
|
||||
lines.where(invoice_id: may_array(filters[:invoice_id]))
|
||||
end
|
||||
|
||||
# @param lines [ActiveRecord::Relation<AccountingLine>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_line_type(lines, filters)
|
||||
return lines if filters[:type].blank?
|
||||
|
||||
lines.where(line_type: may_array(filters[:type]))
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Filter the list of reservations by the given parameters
|
||||
module ReservationsFiltersConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# @param reservations [ActiveRecord::Relation<Reservation>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_after(reservations, filters)
|
||||
return reservations if filters[:after].blank?
|
||||
|
||||
reservations.where('reservations.created_at >= ?', Time.zone.parse(filters[:after]))
|
||||
end
|
||||
|
||||
# @param reservations [ActiveRecord::Relation<Reservation>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_before(reservations, filters)
|
||||
return reservations if filters[:before].blank?
|
||||
|
||||
reservations.where('reservations.created_at <= ?', Time.zone.parse(filters[:before]))
|
||||
end
|
||||
|
||||
# @param reservations [ActiveRecord::Relation<Reservation>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_user(reservations, filters)
|
||||
return reservations if filters[:user_id].blank?
|
||||
|
||||
reservations.where(statistic_profiles: { user_id: may_array(filters[:user_id]) })
|
||||
end
|
||||
|
||||
# @param reservations [ActiveRecord::Relation<Reservation>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_reservable_type(reservations, filters)
|
||||
return reservations if filters[:reservable_type].blank?
|
||||
|
||||
reservations.where(reservable_type: format_type(filters[:reservable_type]))
|
||||
end
|
||||
|
||||
# @param reservations [ActiveRecord::Relation<Reservation>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_reservable_id(reservations, filters)
|
||||
return reservations if filters[:reservable_id].blank?
|
||||
|
||||
reservations.where(reservable_id: may_array(filters[:reservable_id]))
|
||||
end
|
||||
|
||||
# @param type [String]
|
||||
def format_type(type)
|
||||
type.singularize.classify
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Filter the list of subscriptions by the given parameters
|
||||
module SubscriptionsFiltersConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# @param subscriptions [ActiveRecord::Relation<Subscription>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_after(subscriptions, filters)
|
||||
return subscriptions if filters[:after].blank?
|
||||
|
||||
subscriptions.where('created_at >= ?', Time.zone.parse(filters[:after]))
|
||||
end
|
||||
|
||||
# @param subscriptions [ActiveRecord::Relation<Subscription>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_before(subscriptions, filters)
|
||||
return subscriptions if filters[:before].blank?
|
||||
|
||||
subscriptions.where('created_at <= ?', Time.zone.parse(filters[:before]))
|
||||
end
|
||||
|
||||
# @param subscriptions [ActiveRecord::Relation<Subscription>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_user(subscriptions, filters)
|
||||
return subscriptions if filters[:user_id].blank?
|
||||
|
||||
subscriptions.where(statistic_profiles: { user_id: may_array(filters[:user_id]) })
|
||||
end
|
||||
|
||||
# @param subscriptions [ActiveRecord::Relation<Subscription>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_plan(subscriptions, filters)
|
||||
return subscriptions if filters[:plan_id].blank?
|
||||
|
||||
subscriptions.where(plan_id: may_array(filters[:plan_id]))
|
||||
end
|
||||
end
|
||||
end
|
@ -1,9 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'concerns/reservations_filters_concern'
|
||||
|
||||
# public API controller for resources of type Reservation
|
||||
class OpenAPI::V1::ReservationsController < OpenAPI::V1::BaseController
|
||||
extend OpenAPI::ApiDoc
|
||||
include Rails::Pagination
|
||||
include ReservationsFiltersConcern
|
||||
expose_doc
|
||||
|
||||
def index
|
||||
@ -11,9 +14,11 @@ class OpenAPI::V1::ReservationsController < OpenAPI::V1::BaseController
|
||||
.includes(slots_reservations: :slot, statistic_profile: :user)
|
||||
.references(:statistic_profiles)
|
||||
|
||||
@reservations = @reservations.where(statistic_profiles: { user_id: may_array(params[:user_id]) }) if params[:user_id].present?
|
||||
@reservations = @reservations.where(reservable_type: format_type(params[:reservable_type])) if params[:reservable_type].present?
|
||||
@reservations = @reservations.where(reservable_id: may_array(params[:reservable_id])) if params[:reservable_id].present?
|
||||
@reservations = filter_by_after(@reservations, params)
|
||||
@reservations = filter_by_before(@reservations, params)
|
||||
@reservations = filter_by_user(@reservations, params)
|
||||
@reservations = filter_by_reservable_type(@reservations, params)
|
||||
@reservations = filter_by_reservable_id(@reservations, params)
|
||||
|
||||
@reservations = @reservations.page(page).per(per_page)
|
||||
paginate @reservations, per_page: per_page
|
||||
@ -21,10 +26,6 @@ class OpenAPI::V1::ReservationsController < OpenAPI::V1::BaseController
|
||||
|
||||
private
|
||||
|
||||
def format_type(type)
|
||||
type.singularize.classify
|
||||
end
|
||||
|
||||
def page
|
||||
params[:page] || 1
|
||||
end
|
||||
|
@ -1,9 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'concerns/subscriptions_filters_concern'
|
||||
|
||||
# authorized 3rd party softwares can fetch the subscriptions through the OpenAPI
|
||||
class OpenAPI::V1::SubscriptionsController < OpenAPI::V1::BaseController
|
||||
extend OpenAPI::ApiDoc
|
||||
include Rails::Pagination
|
||||
include SubscriptionsFiltersConcern
|
||||
expose_doc
|
||||
|
||||
def index
|
||||
@ -11,10 +14,10 @@ class OpenAPI::V1::SubscriptionsController < OpenAPI::V1::BaseController
|
||||
.includes(:plan, statistic_profile: :user)
|
||||
.references(:statistic_profile, :plan)
|
||||
|
||||
@subscriptions = @subscriptions.where('created_at >= ?', Time.zone.parse(params[:after])) if params[:after].present?
|
||||
@subscriptions = @subscriptions.where('created_at <= ?', Time.zone.parse(params[:before])) if params[:before].present?
|
||||
@subscriptions = @subscriptions.where(plan_id: may_array(params[:plan_id])) if params[:plan_id].present?
|
||||
@subscriptions = @subscriptions.where(statistic_profiles: { user_id: may_array(params[:user_id]) }) if params[:user_id].present?
|
||||
@subscriptions = filter_by_after(@subscriptions, params)
|
||||
@subscriptions = filter_by_before(@subscriptions, params)
|
||||
@subscriptions = filter_by_plan(@subscriptions, params)
|
||||
@subscriptions = filter_by_user(@subscriptions, params)
|
||||
|
||||
@subscriptions = @subscriptions.page(page).per(per_page)
|
||||
paginate @subscriptions, per_page: per_page
|
||||
|
@ -15,6 +15,8 @@ class OpenAPI::V1::ReservationsDoc < OpenAPI::V1::BaseDoc
|
||||
api :GET, "/#{API_VERSION}/reservations", 'Reservations index'
|
||||
description 'Index of reservations made by users, paginated. Ordered by *created_at* descendant.'
|
||||
param_group :pagination
|
||||
param :after, DateTime, optional: true, desc: 'Filter reservations to those created after the given date.'
|
||||
param :before, DateTime, optional: true, desc: 'Filter reservations to those created before the given date.'
|
||||
param :user_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various users.'
|
||||
param :reservable_type, %w[Event Machine Space Training], optional: true, desc: 'Scope the request to a specific type of reservable.'
|
||||
param :reservable_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various reservables.'
|
||||
|
@ -26,6 +26,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
|
||||
"id": 1746,
|
||||
"email": "xxxxxxx@xxxx.com",
|
||||
"created_at": "2016-05-04T17:21:48.403+02:00",
|
||||
"invoicing_profile_id": 7824,
|
||||
"external_id": "J5821-4"
|
||||
"full_name": "xxxx xxxx",
|
||||
"first_name": "xxxx",
|
||||
@ -43,6 +44,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
|
||||
"id": 1745,
|
||||
"email": "xxxxxxx@gmail.com",
|
||||
"created_at": "2016-05-03T15:21:13.125+02:00",
|
||||
"invoicing_profile_id": 7823,
|
||||
"external_id": "J5846-4"
|
||||
"full_name": "xxxxx xxxxx",
|
||||
"first_name": "xxxxx",
|
||||
@ -60,6 +62,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
|
||||
"id": 1744,
|
||||
"email": "xxxxxxx@gmail.com",
|
||||
"created_at": "2016-05-03T13:51:03.223+02:00",
|
||||
"invoicing_profile_id": 7822,
|
||||
"external_id": "J5900-1"
|
||||
"full_name": "xxxxxxx xxxx",
|
||||
"first_name": "xxxxxxx",
|
||||
@ -77,6 +80,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
|
||||
"id": 1743,
|
||||
"email": "xxxxxxxx@setecastronomy.eu",
|
||||
"created_at": "2016-05-03T12:24:38.724+02:00",
|
||||
"invoicing_profile_id": 7821,
|
||||
"external_id": "P4172-4"
|
||||
"full_name": "xxx xxxxxxx",
|
||||
"first_name": "xxx",
|
||||
@ -100,6 +104,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
|
||||
"id": 1746,
|
||||
"email": "xxxxxxxxxxxx",
|
||||
"created_at": "2016-05-04T17:21:48.403+02:00",
|
||||
"invoicing_profile_id": 7820,
|
||||
"external_id": "J5500-4"
|
||||
"full_name": "xxxx xxxxxx",
|
||||
"first_name": "xxxx",
|
||||
@ -117,6 +122,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc
|
||||
"id": 1745,
|
||||
"email": "xxxxxxxxx@gmail.com",
|
||||
"created_at": "2016-05-03T15:21:13.125+02:00",
|
||||
"invoicing_profile_id": 7819,
|
||||
"external_id": null,
|
||||
"full_name": "xxxxx xxxxxx",
|
||||
"first_name": "xxxx",
|
||||
|
@ -3,6 +3,7 @@ import { UseFormRegister, FormState } from 'react-hook-form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabOutputCopy } from '../base/fab-output-copy';
|
||||
import ValidationLib from '../../lib/validation';
|
||||
|
||||
interface Oauth2FormProps<TFieldValues> {
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
@ -16,10 +17,6 @@ interface Oauth2FormProps<TFieldValues> {
|
||||
export const Oauth2Form = <TFieldValues extends FieldValues>({ register, strategyName, formState }: Oauth2FormProps<TFieldValues>) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// regular expression to validate the input fields
|
||||
const endpointRegex = /^\/?([-._~:?#[\]@!$&'()*+,;=%\w]+\/?)*$/;
|
||||
const urlRegex = /^(https?:\/\/)([^.]+)\.(.{2,30})(\/.*)*\/?$/;
|
||||
|
||||
/**
|
||||
* Build the callback URL, based on the strategy name.
|
||||
*/
|
||||
@ -35,26 +32,26 @@ export const Oauth2Form = <TFieldValues extends FieldValues>({ register, strateg
|
||||
register={register}
|
||||
placeholder="https://sso.example.net..."
|
||||
label={t('app.admin.authentication.oauth2_form.common_url')}
|
||||
rules={{ required: true, pattern: urlRegex }}
|
||||
rules={{ required: true, pattern: ValidationLib.urlRegex }}
|
||||
formState={formState} />
|
||||
<FormInput id="providable_attributes.authorization_endpoint"
|
||||
register={register}
|
||||
placeholder="/oauth2/auth..."
|
||||
label={t('app.admin.authentication.oauth2_form.authorization_endpoint')}
|
||||
rules={{ required: true, pattern: endpointRegex }}
|
||||
rules={{ required: true, pattern: ValidationLib.endpointRegex }}
|
||||
formState={formState} />
|
||||
<FormInput id="providable_attributes.token_endpoint"
|
||||
register={register}
|
||||
placeholder="/oauth2/token..."
|
||||
label={t('app.admin.authentication.oauth2_form.token_acquisition_endpoint')}
|
||||
rules={{ required: true, pattern: endpointRegex }}
|
||||
rules={{ required: true, pattern: ValidationLib.endpointRegex }}
|
||||
formState={formState} />
|
||||
<FormInput id="providable_attributes.profile_url"
|
||||
register={register}
|
||||
placeholder="https://exemple.net/user..."
|
||||
label={t('app.admin.authentication.oauth2_form.profile_edition_url')}
|
||||
tooltip={t('app.admin.authentication.oauth2_form.profile_edition_url_help')}
|
||||
rules={{ required: true, pattern: urlRegex }}
|
||||
rules={{ required: true, pattern: ValidationLib.urlRegex }}
|
||||
formState={formState} />
|
||||
<FormInput id="providable_attributes.client_id"
|
||||
register={register}
|
||||
|
@ -12,6 +12,7 @@ import SsoClient from '../../api/external/sso';
|
||||
import { FieldPathValue } from 'react-hook-form/dist/types/path';
|
||||
import { FormMultiSelect } from '../form/form-multi-select';
|
||||
import { difference } from 'lodash';
|
||||
import ValidationLib from '../../lib/validation';
|
||||
|
||||
interface OpenidConnectFormProps<TFieldValues, TContext extends object> {
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
@ -51,10 +52,6 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
|
||||
checkForDiscoveryEndpoint({ target: { value: currentFormValues?.issuer } } as React.ChangeEvent<HTMLInputElement>);
|
||||
}, []);
|
||||
|
||||
// regular expression to validate the input fields
|
||||
const endpointRegex = /^\/?([-._~:?#[\]@!$&'()*+,;=%\w]+\/?)*$/;
|
||||
const urlRegex = /^(https?:\/\/)([^.]+)\.(.{2,30})(\/.*)*\/?$/;
|
||||
|
||||
/**
|
||||
* If the discovery endpoint is available, the user will be able to choose to use it or not.
|
||||
* Otherwise, he will need to end the client configuration manually.
|
||||
@ -109,7 +106,7 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
|
||||
label={t('app.admin.authentication.openid_connect_form.issuer')}
|
||||
placeholder="https://sso.exemple.com"
|
||||
tooltip={t('app.admin.authentication.openid_connect_form.issuer_help')}
|
||||
rules={{ required: true, pattern: urlRegex }}
|
||||
rules={{ required: true, pattern: ValidationLib.urlRegex }}
|
||||
onChange={checkForDiscoveryEndpoint}
|
||||
debounce={400}
|
||||
warning={!discoveryAvailable && { message: t('app.admin.authentication.openid_connect_form.discovery_unavailable') } }
|
||||
@ -161,7 +158,7 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
|
||||
placeholder="https://sso.exemple.com/my-account"
|
||||
label={t('app.admin.authentication.openid_connect_form.profile_edition_url')}
|
||||
tooltip={t('app.admin.authentication.openid_connect_form.profile_edition_url_help')}
|
||||
rules={{ required: false, pattern: urlRegex }}
|
||||
rules={{ required: false, pattern: ValidationLib.urlRegex }}
|
||||
formState={formState} />
|
||||
<h4>{t('app.admin.authentication.openid_connect_form.client_options')}</h4>
|
||||
<FormInput id="providable_attributes.client__identifier"
|
||||
@ -178,31 +175,31 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
|
||||
<FormInput id="providable_attributes.client__authorization_endpoint"
|
||||
label={t('app.admin.authentication.openid_connect_form.client__authorization_endpoint')}
|
||||
placeholder="/authorize"
|
||||
rules={{ required: !currentFormValues?.discovery, pattern: endpointRegex }}
|
||||
rules={{ required: !currentFormValues?.discovery, pattern: ValidationLib.endpointRegex }}
|
||||
formState={formState}
|
||||
register={register} />
|
||||
<FormInput id="providable_attributes.client__token_endpoint"
|
||||
label={t('app.admin.authentication.openid_connect_form.client__token_endpoint')}
|
||||
placeholder="/token"
|
||||
rules={{ required: !currentFormValues?.discovery, pattern: endpointRegex }}
|
||||
rules={{ required: !currentFormValues?.discovery, pattern: ValidationLib.endpointRegex }}
|
||||
formState={formState}
|
||||
register={register} />
|
||||
<FormInput id="providable_attributes.client__userinfo_endpoint"
|
||||
label={t('app.admin.authentication.openid_connect_form.client__userinfo_endpoint')}
|
||||
placeholder="/userinfo"
|
||||
rules={{ required: !currentFormValues?.discovery, pattern: endpointRegex }}
|
||||
rules={{ required: !currentFormValues?.discovery, pattern: ValidationLib.endpointRegex }}
|
||||
formState={formState}
|
||||
register={register} />
|
||||
{currentFormValues?.client_auth_method === 'jwks' && <FormInput id="providable_attributes.client__jwks_uri"
|
||||
label={t('app.admin.authentication.openid_connect_form.client__jwks_uri')}
|
||||
rules={{ required: currentFormValues.client_auth_method === 'jwks', pattern: endpointRegex }}
|
||||
rules={{ required: currentFormValues.client_auth_method === 'jwks', pattern: ValidationLib.endpointRegex }}
|
||||
formState={formState}
|
||||
placeholder="/jwk"
|
||||
register={register} />}
|
||||
<FormInput id="providable_attributes.client__end_session_endpoint"
|
||||
label={t('app.admin.authentication.openid_connect_form.client__end_session_endpoint')}
|
||||
tooltip={t('app.admin.authentication.openid_connect_form.client__end_session_endpoint_help')}
|
||||
rules={{ pattern: endpointRegex }}
|
||||
rules={{ pattern: ValidationLib.endpointRegex }}
|
||||
formState={formState}
|
||||
register={register} />
|
||||
</div>}
|
||||
|
@ -41,7 +41,7 @@ const PrepaidPacksPanel: React.FC<PrepaidPacksPanelProps> = ({ user, onError })
|
||||
const { handleSubmit, control, formState } = useForm<{ machine_id: number }>();
|
||||
|
||||
useEffect(() => {
|
||||
UserPackAPI.index({ user_id: user.id })
|
||||
UserPackAPI.index({ user_id: user.id, history: true })
|
||||
.then(setUserPacks)
|
||||
.catch(onError);
|
||||
SettingAPI.get('renew_pack_threshold')
|
||||
@ -106,7 +106,7 @@ const PrepaidPacksPanel: React.FC<PrepaidPacksPanelProps> = ({ user, onError })
|
||||
*/
|
||||
const onPackBoughtSuccess = () => {
|
||||
togglePacksModal();
|
||||
UserPackAPI.index({ user_id: user.id })
|
||||
UserPackAPI.index({ user_id: user.id, history: true })
|
||||
.then(setUserPacks)
|
||||
.catch(onError);
|
||||
};
|
||||
@ -125,19 +125,21 @@ const PrepaidPacksPanel: React.FC<PrepaidPacksPanelProps> = ({ user, onError })
|
||||
<div className='prepaid-packs-list-item'>
|
||||
<p className='name'>{pack.prepaid_pack.priceable.name}</p>
|
||||
{FormatLib.date(pack.expires_at) && <p className="end">{FormatLib.date(pack.expires_at)}</p>}
|
||||
<p className="countdown"><span>{pack.minutes_used / 60}H</span> / {pack.prepaid_pack.minutes / 60}H</p>
|
||||
<p className="countdown"><span>{(pack.prepaid_pack.minutes - pack.minutes_used) / 60}H</span> / {pack.prepaid_pack.minutes / 60}H</p>
|
||||
</div>
|
||||
</div>
|
||||
{ /* usage history is not saved for now
|
||||
{pack.history?.length > 0 &&
|
||||
<div className="prepaid-packs-list is-history">
|
||||
<span className='prepaid-packs-list-label'>{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.history')}</span>
|
||||
|
||||
<div className='prepaid-packs-list-item'>
|
||||
<p className='name'>00{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.consumed_hours')}</p>
|
||||
<p className="date">00/00/00</p>
|
||||
</div>
|
||||
{pack.history.map(prepaidReservation => (
|
||||
<div className='prepaid-packs-list-item' key={prepaidReservation.id}>
|
||||
<p className='name'>{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.consumed_hours', { COUNT: prepaidReservation.consumed_minutes / 60 })}</p>
|
||||
<p className="date">{FormatLib.date(prepaidReservation.reservation_date)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
*/ }
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { FormSwitch } from '../form/form-switch';
|
||||
import { FormRichText } from '../form/form-rich-text';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { SettingName, SettingValue } from '../../models/setting';
|
||||
import ValidationLib from '../../lib/validation';
|
||||
export type EditorialKeys = 'active_text_block' | 'text_block' | 'active_cta' | 'cta_label' | 'cta_url';
|
||||
|
||||
interface EditorialBlockFormProps {
|
||||
@ -15,9 +16,6 @@ interface EditorialBlockFormProps {
|
||||
keys: Record<EditorialKeys, SettingName>
|
||||
}
|
||||
|
||||
// regular expression to validate the input fields
|
||||
const urlRegex = /^(https?:\/\/)([^.]+)\.(.{2,30})(\/.*)*\/?$/;
|
||||
|
||||
/**
|
||||
* Allows to create a formatted text and optional cta button in a form block, to be included in a resource form managed by react-hook-form.
|
||||
*/
|
||||
@ -78,7 +76,7 @@ export const EditorialBlockForm: React.FC<EditorialBlockFormProps> = ({ register
|
||||
formState={formState}
|
||||
rules={{
|
||||
required: { value: isActiveCta, message: t('app.admin.editorial_block_form.url_is_required') },
|
||||
pattern: { value: urlRegex, message: t('app.admin.editorial_block_form.url_must_be_safe') }
|
||||
pattern: { value: ValidationLib.urlRegex, message: t('app.admin.editorial_block_form.url_must_be_safe') }
|
||||
}}
|
||||
label={t('app.admin.editorial_block_form.cta_url')} />
|
||||
</>}
|
||||
|
@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import MachineCategoryAPI from '../../api/machine-category';
|
||||
|
||||
interface DeleteMachineCategoryModalProps {
|
||||
isOpen: boolean,
|
||||
machineCategoryId: number,
|
||||
toggleModal: () => void,
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal dialog to remove a requested machine category
|
||||
*/
|
||||
export const DeleteMachineCategoryModal: React.FC<DeleteMachineCategoryModalProps> = ({ isOpen, toggleModal, onSuccess, machineCategoryId, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
/**
|
||||
* The user has confirmed the deletion of the requested machine category
|
||||
*/
|
||||
const handleDeleteMachineCategory = async (): Promise<void> => {
|
||||
try {
|
||||
await MachineCategoryAPI.destroy(machineCategoryId);
|
||||
onSuccess(t('app.admin.delete_machine_category_modal.deleted'));
|
||||
} catch (e) {
|
||||
onError(t('app.admin.delete_machine_category_modal.unable_to_delete') + e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal title={t('app.admin.delete_machine_category_modal.confirmation_required')}
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.admin.delete_machine_category_modal.confirm')}
|
||||
onConfirm={handleDeleteMachineCategory}
|
||||
className="delete-machine-category-modal">
|
||||
<p>{t('app.admin.delete_machine_category_modal.confirm_machine_category')}</p>
|
||||
</FabModal>
|
||||
);
|
||||
};
|
@ -1,15 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { MachineCategory } from '../../models/machine-category';
|
||||
import { Machine } from '../../models/machine';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../base/loader';
|
||||
import MachineCategoryAPI from '../../api/machine-category';
|
||||
import MachineAPI from '../../api/machine';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { MachineCategoryModal } from './machine-category-modal';
|
||||
import { DeleteMachineCategoryModal } from './delete-machine-category-modal';
|
||||
import { EditDestroyButtons } from '../base/edit-destroy-buttons';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -26,25 +24,16 @@ export const MachineCategoriesList: React.FC<MachineCategoriesListProps> = ({ on
|
||||
|
||||
// shown machine categories
|
||||
const [machineCategories, setMachineCategories] = useState<Array<MachineCategory>>([]);
|
||||
// all machines, for assign to category
|
||||
const [machines, setMachines] = useState<Array<Machine>>([]);
|
||||
// creation/edition modal
|
||||
const [modalIsOpen, setModalIsOpen] = useState<boolean>(false);
|
||||
// currently added/edited category
|
||||
const [machineCategory, setMachineCategory] = useState<MachineCategory>(null);
|
||||
// deletion modal
|
||||
const [destroyModalIsOpen, setDestroyModalIsOpen] = useState<boolean>(false);
|
||||
// currently deleted machine category
|
||||
const [machineCategoryId, setMachineCategoryId] = useState<number>(null);
|
||||
|
||||
// retrieve the full list of machine categories on component mount
|
||||
useEffect(() => {
|
||||
MachineCategoryAPI.index()
|
||||
.then(setMachineCategories)
|
||||
.catch(onError);
|
||||
MachineAPI.index({ category: 'none' })
|
||||
.then(setMachines)
|
||||
.catch(onError);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
@ -59,7 +48,6 @@ export const MachineCategoriesList: React.FC<MachineCategoriesListProps> = ({ on
|
||||
*/
|
||||
const onSaveTypeSuccess = (message: string): void => {
|
||||
setModalIsOpen(false);
|
||||
MachineAPI.index({ category: 'none' }).then(setMachines).catch(onError);
|
||||
MachineCategoryAPI.index().then(data => {
|
||||
setMachineCategories(data);
|
||||
onSuccess(message);
|
||||
@ -86,28 +74,10 @@ export const MachineCategoriesList: React.FC<MachineCategoriesListProps> = ({ on
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Init the process of deleting a machine category (ask for confirmation)
|
||||
*/
|
||||
const destroyMachineCategory = (id: number): () => void => {
|
||||
return (): void => {
|
||||
setMachineCategoryId(id);
|
||||
setDestroyModalIsOpen(true);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Open/closes the confirmation before deletion modal
|
||||
*/
|
||||
const toggleDestroyModal = (): void => {
|
||||
setDestroyModalIsOpen(!destroyModalIsOpen);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggred when the current machine category was successfully deleted
|
||||
*/
|
||||
const onDestroySuccess = (message: string): void => {
|
||||
setDestroyModalIsOpen(false);
|
||||
MachineCategoryAPI.index().then(data => {
|
||||
setMachineCategories(data);
|
||||
onSuccess(message);
|
||||
@ -125,16 +95,10 @@ export const MachineCategoriesList: React.FC<MachineCategoriesListProps> = ({ on
|
||||
</div>
|
||||
</header>
|
||||
<MachineCategoryModal isOpen={modalIsOpen}
|
||||
machines={machines}
|
||||
machineCategory={machineCategory}
|
||||
toggleModal={toggleCreateAndEditModal}
|
||||
onSuccess={onSaveTypeSuccess}
|
||||
onError={onError} />
|
||||
<DeleteMachineCategoryModal isOpen={destroyModalIsOpen}
|
||||
machineCategoryId={machineCategoryId}
|
||||
toggleModal={toggleDestroyModal}
|
||||
onSuccess={onDestroySuccess}
|
||||
onError={onError}/>
|
||||
<table className="machine-categories-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -155,12 +119,12 @@ export const MachineCategoriesList: React.FC<MachineCategoriesListProps> = ({ on
|
||||
</td>
|
||||
<td>
|
||||
<div className="buttons">
|
||||
<FabButton className="edit-btn" onClick={editMachineCategory(category)}>
|
||||
<i className="fa fa-edit" /> {t('app.admin.machine_categories_list.edit')}
|
||||
</FabButton>
|
||||
<FabButton className="delete-btn" onClick={destroyMachineCategory(category.id)}>
|
||||
<i className="fa fa-trash" />
|
||||
</FabButton>
|
||||
<EditDestroyButtons onDeleteSuccess={onDestroySuccess}
|
||||
onError={onError}
|
||||
onEdit={editMachineCategory(category)}
|
||||
itemId={category.id}
|
||||
itemType={t('app.admin.machine_categories_list.machine_category')}
|
||||
apiDestroy={MachineCategoryAPI.destroy} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1,25 +1,36 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||
import { MachineCategory } from '../../models/machine-category';
|
||||
import { Machine } from '../../models/machine';
|
||||
import MachineCategoryAPI from '../../api/machine-category';
|
||||
import { MachineCategoryForm } from './machine-category-form';
|
||||
import MachineAPI from '../../api/machine';
|
||||
|
||||
interface MachineCategoryModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
machines: Array<Machine>,
|
||||
machineCategory?: MachineCategory,
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal dialog to create/edit a machine category
|
||||
*/
|
||||
export const MachineCategoryModal: React.FC<MachineCategoryModalProps> = ({ isOpen, toggleModal, onSuccess, onError, machines, machineCategory }) => {
|
||||
export const MachineCategoryModal: React.FC<MachineCategoryModalProps> = ({ isOpen, toggleModal, onSuccess, onError, machineCategory }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
// all machines, to assign to the category
|
||||
const [machines, setMachines] = useState<Array<Machine>>([]);
|
||||
|
||||
// retrieve the full list of machines on component mount
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
MachineAPI.index({ category: [machineCategory?.id, 'none'] })
|
||||
.then(setMachines)
|
||||
.catch(onError);
|
||||
}, [isOpen]);
|
||||
|
||||
/**
|
||||
* Save the current machine category to the API
|
||||
|
@ -75,7 +75,7 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
|
||||
};
|
||||
|
||||
/**
|
||||
* When the user clicks on the edition button, query the full data of the current pack from the API, then open te edition modal
|
||||
* When the user clicks on the edition button, query the full data of the current pack from the API, then open the edition modal
|
||||
*/
|
||||
const handleRequestEdit = (pack: PrepaidPack): void => {
|
||||
PrepaidPackAPI.get(pack.id)
|
||||
|
@ -7,6 +7,7 @@ import Icons from '../../../../images/social-icons.svg';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ValidationLib from '../../lib/validation';
|
||||
|
||||
interface EditSocialsProps<TFieldValues> {
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
@ -21,8 +22,6 @@ interface EditSocialsProps<TFieldValues> {
|
||||
*/
|
||||
export const EditSocials = <TFieldValues extends FieldValues>({ register, setValue, networks, formState, disabled }: EditSocialsProps<TFieldValues>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
// regular expression to validate the input fields
|
||||
const urlRegex = /^(https?:\/\/)([^.]+)\.(.{2,30})(\/.*)*\/?$/;
|
||||
|
||||
const initSelectedNetworks = networks.filter(el => !['', null, undefined].includes(el.url));
|
||||
const [selectedNetworks, setSelectedNetworks] = useState(initSelectedNetworks);
|
||||
@ -72,7 +71,7 @@ export const EditSocials = <TFieldValues extends FieldValues>({ register, setVal
|
||||
register={register}
|
||||
rules= {{
|
||||
pattern: {
|
||||
value: urlRegex,
|
||||
value: ValidationLib.urlRegex,
|
||||
message: t('app.shared.edit_socials.website_invalid')
|
||||
}
|
||||
}}
|
||||
|
@ -12,6 +12,7 @@ import Icons from '../../../../images/social-icons.svg';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import ValidationLib from '../../lib/validation';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -26,8 +27,6 @@ interface FabSocialsProps {
|
||||
*/
|
||||
export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, onSuccess }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
// regular expression to validate the input fields
|
||||
const urlRegex = /^(https?:\/\/)([\da-z.-]+)\.([-a-z\d.]{2,30})([/\w .-]*)*\/?$/;
|
||||
|
||||
const { handleSubmit, register, setValue, formState } = useForm();
|
||||
|
||||
@ -109,7 +108,7 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
|
||||
register={register}
|
||||
rules={{
|
||||
pattern: {
|
||||
value: urlRegex,
|
||||
value: ValidationLib.urlRegex,
|
||||
message: t('app.shared.fab_socials.website_invalid')
|
||||
}
|
||||
}}
|
||||
|
@ -32,6 +32,7 @@ import { ProfileCustomField } from '../../models/profile-custom-field';
|
||||
import { SettingName } from '../../models/setting';
|
||||
import SettingAPI from '../../api/setting';
|
||||
import { SelectOption } from '../../models/select';
|
||||
import ValidationLib from '../../lib/validation';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -55,10 +56,6 @@ interface UserProfileFormProps {
|
||||
export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size, user, operator, className, onError, onSuccess, showGroupInput, showTermsAndConditionsInput, showTrainingsInput, showTagsInput }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
// regular expression to validate the input fields
|
||||
const phoneRegex = /^((00|\+)\d{2,3})?[\d -]{4,14}$/;
|
||||
const urlRegex = /^(https?:\/\/)([^.]+)\.(.{2,30})(\/.*)*\/?$/;
|
||||
|
||||
const { handleSubmit, register, control, formState, setValue, reset } = useForm<User>({ defaultValues: { ...user } });
|
||||
const output = useWatch<User>({ control });
|
||||
|
||||
@ -215,7 +212,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
register={register}
|
||||
rules={{
|
||||
pattern: {
|
||||
value: phoneRegex,
|
||||
value: ValidationLib.phoneRegex,
|
||||
message: t('app.shared.user_profile_form.phone_number_invalid')
|
||||
},
|
||||
required: fieldsSettings.get('phone_required') === 'true'
|
||||
@ -314,7 +311,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
register={register}
|
||||
rules={{
|
||||
pattern: {
|
||||
value: urlRegex,
|
||||
value: ValidationLib.urlRegex,
|
||||
message: t('app.shared.user_profile_form.website_invalid')
|
||||
}
|
||||
}}
|
||||
|
6
app/frontend/src/javascript/lib/validation.ts
Normal file
6
app/frontend/src/javascript/lib/validation.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// Provides regular expressions to validate user inputs
|
||||
export default class ValidationLib {
|
||||
static urlRegex = /^(https?:\/\/)(([^.]+)\.)+(.{2,30})(\/.*)*\/?$/;
|
||||
static endpointRegex = /^\/?([-._~:?#[\]@!$&'()*+,;=%\w]+\/?)*$/;
|
||||
static phoneRegex = /^((00|\+)\d{2,3})?[\d -]{4,14}$/;
|
||||
}
|
@ -5,7 +5,7 @@ import { AdvancedAccounting } from './advanced-accounting';
|
||||
|
||||
export interface MachineIndexFilter extends ApiFilter {
|
||||
disabled?: boolean,
|
||||
category?: number | 'none'
|
||||
category?: number | 'none' | Array<number|'none'>
|
||||
}
|
||||
|
||||
export interface Machine {
|
||||
|
@ -4,7 +4,8 @@ import { ApiFilter } from './api';
|
||||
export interface UserPackIndexFilter extends ApiFilter {
|
||||
user_id: number,
|
||||
priceable_type?: string,
|
||||
priceable_id?: number
|
||||
priceable_id?: number,
|
||||
history?: boolean
|
||||
}
|
||||
|
||||
export interface UserPack {
|
||||
@ -17,5 +18,11 @@ export interface UserPack {
|
||||
priceable: {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
},
|
||||
history?: Array<{
|
||||
id: number,
|
||||
consumed_minutes: number,
|
||||
reservation_id: number,
|
||||
reservation_date: TDateISO
|
||||
}>
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
.edit-destroy-buttons {
|
||||
border-radius: var(--border-radius-sm);
|
||||
overflow: hidden;
|
||||
button {
|
||||
@include btn;
|
||||
border-radius: 0;
|
||||
@ -12,4 +14,4 @@
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
.events {
|
||||
.events-list-page {
|
||||
max-width: 1600px;
|
||||
margin: 2rem;
|
||||
padding-bottom: 6rem;
|
||||
|
@ -14,18 +14,6 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
button {
|
||||
border-radius: 5;
|
||||
&:hover { opacity: 0.75; }
|
||||
}
|
||||
.edit-btn {
|
||||
color: var(--gray-hard-darkest);
|
||||
margin-right: 10px;
|
||||
}
|
||||
.delete-btn {
|
||||
color: var(--gray-soft-lightest);
|
||||
background: var(--main);
|
||||
}
|
||||
}
|
||||
|
||||
.machine-categories-table {
|
||||
|
@ -19,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<section class="events">
|
||||
<section class="events-list-page">
|
||||
<events-editorial-block on-error="onError"></events-editorial-block>
|
||||
|
||||
<div class="row">
|
||||
|
@ -74,7 +74,7 @@ module ApplicationHelper
|
||||
def may_array(param)
|
||||
return param if param.is_a?(Array)
|
||||
|
||||
return param unless param&.chars&.first == '[' && param&.chars&.last == ']'
|
||||
return param unless param.chars&.first == '[' && param.chars&.last == ']'
|
||||
|
||||
param.gsub(/[\[\]]/i, '').split(',')
|
||||
end
|
||||
|
7
app/models/prepaid_pack_reservation.rb
Normal file
7
app/models/prepaid_pack_reservation.rb
Normal file
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Save the association between a Reservation and a PrepaidPack to keep the usage history.
|
||||
class PrepaidPackReservation < ApplicationRecord
|
||||
belongs_to :statistic_profile_prepaid_pack
|
||||
belongs_to :reservation
|
||||
end
|
@ -21,6 +21,8 @@ class Reservation < ApplicationRecord
|
||||
has_many :invoice_items, as: :object, dependent: :destroy
|
||||
has_one :payment_schedule_object, as: :object, dependent: :destroy
|
||||
|
||||
has_many :prepaid_pack_reservations, dependent: :destroy
|
||||
|
||||
validates :reservable_id, :reservable_type, presence: true
|
||||
validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
|
||||
validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) }
|
||||
|
@ -8,6 +8,7 @@ class StatisticProfilePrepaidPack < ApplicationRecord
|
||||
|
||||
has_many :invoice_items, as: :object, dependent: :destroy
|
||||
has_one :payment_schedule_object, as: :object, dependent: :destroy
|
||||
has_many :prepaid_pack_reservations, dependent: :restrict_with_error
|
||||
|
||||
before_create :set_expiration_date
|
||||
|
||||
|
@ -44,7 +44,7 @@ class Accounting::AccountingCodeService
|
||||
|
||||
if type == :code
|
||||
item_code = Setting.get('advanced_accounting') ? invoice_item.object.reservable.advanced_accounting&.send(section) : nil
|
||||
return Setting.get("accounting_#{invoice_item.object.reservable_type}_code") if item_code.nil? && section == :code
|
||||
return Setting.get("accounting_#{invoice_item.object.reservable_type}_code") if item_code.blank? && section == :code
|
||||
|
||||
item_code
|
||||
else
|
||||
@ -58,7 +58,7 @@ class Accounting::AccountingCodeService
|
||||
|
||||
if type == :code
|
||||
item_code = Setting.get('advanced_accounting') ? invoice_item.object.plan.advanced_accounting&.send(section) : nil
|
||||
return Setting.get('accounting_subscription_code') if item_code.nil? && section == :code
|
||||
return Setting.get('accounting_subscription_code') if item_code.blank? && section == :code
|
||||
|
||||
item_code
|
||||
else
|
||||
@ -72,7 +72,7 @@ class Accounting::AccountingCodeService
|
||||
|
||||
if type == :code
|
||||
item_code = Setting.get('advanced_accounting') ? invoice_item.object.orderable.advanced_accounting&.send(section) : nil
|
||||
return Setting.get('accounting_Product_code') if item_code.nil? && section == :code
|
||||
return Setting.get('accounting_Product_code') if item_code.blank? && section == :code
|
||||
|
||||
item_code
|
||||
else
|
||||
|
@ -17,7 +17,7 @@ class Checkout::PaymentService
|
||||
|
||||
CouponService.new.validate(coupon_code, order.statistic_profile.user.id)
|
||||
|
||||
amount = debit_amount(order)
|
||||
amount = debit_amount(order, coupon_code)
|
||||
if (operator.privileged? && operator != order.statistic_profile.user) || amount.zero?
|
||||
Payments::LocalService.new.payment(order, coupon_code)
|
||||
elsif Stripe::Helper.enabled? && payment_id.present?
|
||||
|
@ -3,6 +3,8 @@
|
||||
# Provides methods for Machines
|
||||
class MachineService
|
||||
class << self
|
||||
include ApplicationHelper
|
||||
|
||||
# @param filters [ActionController::Parameters]
|
||||
def list(filters)
|
||||
sort_by = Setting.get('machines_sort_by') || 'default'
|
||||
@ -15,7 +17,7 @@ class MachineService
|
||||
machines = machines.where(deleted_at: nil)
|
||||
|
||||
machines = filter_by_disabled(machines, filters)
|
||||
filter_by_category(machines, filters)
|
||||
filter_by_categories(machines, filters)
|
||||
end
|
||||
|
||||
private
|
||||
@ -31,12 +33,10 @@ class MachineService
|
||||
|
||||
# @param machines [ActiveRecord::Relation<Machine>]
|
||||
# @param filters [ActionController::Parameters]
|
||||
def filter_by_category(machines, filters)
|
||||
def filter_by_categories(machines, filters)
|
||||
return machines if filters[:category].blank?
|
||||
|
||||
return machines.where(machine_category_id: nil) if filters[:category] == 'none'
|
||||
|
||||
machines.where(machine_category_id: filters[:category])
|
||||
machines.where(machine_category_id: filters[:category].split(',').map { |id| id == 'none' ? nil : id })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -7,6 +7,7 @@ class PrepaidPackService
|
||||
# @option filters [Integer] :group_id
|
||||
# @option filters [Integer] :priceable_id
|
||||
# @option filters [String] :priceable_type 'Machine' | 'Space'
|
||||
# @return [ActiveRecord::Relation<PrepaidPack>]
|
||||
def list(filters)
|
||||
packs = PrepaidPack.where(nil)
|
||||
|
||||
@ -25,6 +26,7 @@ class PrepaidPackService
|
||||
# return the not expired packs for the given item bought by the given user
|
||||
# @param user [User]
|
||||
# @param priceable [Machine,Space,NilClass]
|
||||
# @return [ActiveRecord::Relation<StatisticProfilePrepaidPack>]
|
||||
def user_packs(user, priceable = nil)
|
||||
sppp = StatisticProfilePrepaidPack.includes(:prepaid_pack)
|
||||
.references(:prepaid_packs)
|
||||
@ -60,12 +62,12 @@ class PrepaidPackService
|
||||
packs = user_packs(user, reservation.reservable).order(minutes_used: :desc)
|
||||
packs.each do |pack|
|
||||
pack_available = pack.prepaid_pack.minutes - pack.minutes_used
|
||||
remaining = pack_available - consumed
|
||||
remaining = 0 if remaining.negative?
|
||||
pack_consumed = pack.prepaid_pack.minutes - remaining
|
||||
pack.update(minutes_used: pack_consumed)
|
||||
remaining = consumed > pack_available ? 0 : pack_available - consumed
|
||||
pack.update(minutes_used: pack.prepaid_pack.minutes - remaining)
|
||||
|
||||
pack_consumed = consumed > pack_available ? pack_available : consumed
|
||||
consumed -= pack_consumed
|
||||
PrepaidPackReservation.create!(statistic_profile_prepaid_pack: pack, reservation: reservation, consumed_minutes: pack_consumed)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -8,4 +8,10 @@ json.array!(@user_packs) do |user_pack|
|
||||
json.extract! user_pack.prepaid_pack.priceable, :name
|
||||
end
|
||||
end
|
||||
if @history
|
||||
json.history user_pack.prepaid_pack_reservations do |ppr|
|
||||
json.extract! ppr, :id, :consumed_minutes, :reservation_id
|
||||
json.reservation_date ppr.reservation.created_at
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -5,6 +5,7 @@ json.extract! user.profile, :full_name, :first_name, :last_name if user.associat
|
||||
json.gender user.statistic_profile.gender ? 'man' : 'woman'
|
||||
|
||||
if user.association(:invoicing_profile).loaded?
|
||||
json.invoicing_profile_id user.invoicing_profile.id
|
||||
json.external_id user.invoicing_profile.external_id
|
||||
json.organization !user.invoicing_profile.organization.nil?
|
||||
json.address user.invoicing_profile.invoicing_address
|
||||
|
@ -28,7 +28,7 @@ de:
|
||||
add_a_machine_category: "Add a machine category"
|
||||
name: "Name"
|
||||
machines_number: "Number of machines"
|
||||
edit: "Edit"
|
||||
machine_category: "Machine category"
|
||||
machine_category_modal:
|
||||
new_machine_category: "New category"
|
||||
edit_machine_category: "Edit category"
|
||||
@ -40,12 +40,6 @@ de:
|
||||
name: "Name of category"
|
||||
assigning_machines: "Assign machines to this category"
|
||||
save: "Save"
|
||||
delete_machine_category_modal:
|
||||
confirmation_required: "Confirmation required"
|
||||
confirm: "Confirm"
|
||||
deleted: "The machine category has been successfully deleted."
|
||||
unable_to_delete: "Unable to delete the machine category: "
|
||||
confirm_machine_category: "Do you really want to remove this machine category?"
|
||||
machine_form:
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} machine"
|
||||
watch_out_when_creating_a_new_machine_its_prices_are_initialized_at_0_for_all_subscriptions: "Watch out! When creating a new machine, its prices are initialized at 0 for all subscriptions."
|
||||
|
@ -28,7 +28,7 @@ en:
|
||||
add_a_machine_category: "Add a machine category"
|
||||
name: "Name"
|
||||
machines_number: "Number of machines"
|
||||
edit: "Edit"
|
||||
machine_category: "Machine category"
|
||||
machine_category_modal:
|
||||
new_machine_category: "New category"
|
||||
edit_machine_category: "Edit category"
|
||||
@ -40,12 +40,6 @@ en:
|
||||
name: "Name of category"
|
||||
assigning_machines: "Assign machines to this category"
|
||||
save: "Save"
|
||||
delete_machine_category_modal:
|
||||
confirmation_required: "Confirmation required"
|
||||
confirm: "Confirm"
|
||||
deleted: "The machine category has been successfully deleted."
|
||||
unable_to_delete: "Unable to delete the machine category: "
|
||||
confirm_machine_category: "Do you really want to remove this machine category?"
|
||||
machine_form:
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} machine"
|
||||
watch_out_when_creating_a_new_machine_its_prices_are_initialized_at_0_for_all_subscriptions: "Watch out! When creating a new machine, its prices are initialized at 0 for all subscriptions."
|
||||
|
@ -28,7 +28,7 @@ es:
|
||||
add_a_machine_category: "Add a machine category"
|
||||
name: "Name"
|
||||
machines_number: "Number of machines"
|
||||
edit: "Edit"
|
||||
machine_category: "Machine category"
|
||||
machine_category_modal:
|
||||
new_machine_category: "New category"
|
||||
edit_machine_category: "Edit category"
|
||||
@ -40,12 +40,6 @@ es:
|
||||
name: "Name of category"
|
||||
assigning_machines: "Assign machines to this category"
|
||||
save: "Save"
|
||||
delete_machine_category_modal:
|
||||
confirmation_required: "Confirmation required"
|
||||
confirm: "Confirm"
|
||||
deleted: "The machine category has been successfully deleted."
|
||||
unable_to_delete: "Unable to delete the machine category: "
|
||||
confirm_machine_category: "Do you really want to remove this machine category?"
|
||||
machine_form:
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} machine"
|
||||
watch_out_when_creating_a_new_machine_its_prices_are_initialized_at_0_for_all_subscriptions: "Watch out! When creating a new machine, its prices are initialized at 0 for all subscriptions."
|
||||
|
@ -28,7 +28,7 @@ fr:
|
||||
add_a_machine_category: "Ajouter une catégorie de machine"
|
||||
name: "Nom"
|
||||
machines_number: "Nombre de machines"
|
||||
edit: "Modifier"
|
||||
machine_category: "Catégorie de machines"
|
||||
machine_category_modal:
|
||||
new_machine_category: "Nouvelle catégorie"
|
||||
edit_machine_category: "Modifier la catégorie"
|
||||
@ -40,12 +40,6 @@ fr:
|
||||
name: "Nom de la catégorie"
|
||||
assigning_machines: "Affectation des machines à cette catégorie"
|
||||
save: "Enregistrer"
|
||||
delete_machine_category_modal:
|
||||
confirmation_required: "Confirmation requise"
|
||||
confirm: "Confirmer"
|
||||
deleted: "La catégorie de machine a bien été supprimée."
|
||||
unable_to_delete: "Impossible de supprimer la catégorie de machines : "
|
||||
confirm_machine_category: "Voulez-vous vraiment supprimer cette catégorie de machine ?"
|
||||
machine_form:
|
||||
ACTION_title: "{ACTION, select, create{Nouvelle} other{Mettre à jour la}} machine"
|
||||
watch_out_when_creating_a_new_machine_its_prices_are_initialized_at_0_for_all_subscriptions: "Attention ! Lors de la création d'une machine, ses tarifs de réservation sont initialisés à zéro pour tous les abonnements."
|
||||
|
@ -28,7 +28,7 @@
|
||||
add_a_machine_category: "Add a machine category"
|
||||
name: "Name"
|
||||
machines_number: "Number of machines"
|
||||
edit: "Edit"
|
||||
machine_category: "Machine category"
|
||||
machine_category_modal:
|
||||
new_machine_category: "New category"
|
||||
edit_machine_category: "Edit category"
|
||||
@ -40,12 +40,6 @@
|
||||
name: "Name of category"
|
||||
assigning_machines: "Assign machines to this category"
|
||||
save: "Save"
|
||||
delete_machine_category_modal:
|
||||
confirmation_required: "Confirmation required"
|
||||
confirm: "Confirm"
|
||||
deleted: "The machine category has been successfully deleted."
|
||||
unable_to_delete: "Unable to delete the machine category: "
|
||||
confirm_machine_category: "Do you really want to remove this machine category?"
|
||||
machine_form:
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} machine"
|
||||
watch_out_when_creating_a_new_machine_its_prices_are_initialized_at_0_for_all_subscriptions: "Watch out! When creating a new machine, its prices are initialized at 0 for all subscriptions."
|
||||
|
@ -28,7 +28,7 @@ pt:
|
||||
add_a_machine_category: "Add a machine category"
|
||||
name: "Name"
|
||||
machines_number: "Number of machines"
|
||||
edit: "Edit"
|
||||
machine_category: "Machine category"
|
||||
machine_category_modal:
|
||||
new_machine_category: "New category"
|
||||
edit_machine_category: "Edit category"
|
||||
@ -40,12 +40,6 @@ pt:
|
||||
name: "Name of category"
|
||||
assigning_machines: "Assign machines to this category"
|
||||
save: "Save"
|
||||
delete_machine_category_modal:
|
||||
confirmation_required: "Confirmation required"
|
||||
confirm: "Confirm"
|
||||
deleted: "The machine category has been successfully deleted."
|
||||
unable_to_delete: "Unable to delete the machine category: "
|
||||
confirm_machine_category: "Do you really want to remove this machine category?"
|
||||
machine_form:
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} machine"
|
||||
watch_out_when_creating_a_new_machine_its_prices_are_initialized_at_0_for_all_subscriptions: "Watch out! When creating a new machine, its prices are initialized at 0 for all subscriptions."
|
||||
|
@ -28,7 +28,7 @@ zu:
|
||||
add_a_machine_category: "crwdns36071:0crwdne36071:0"
|
||||
name: "crwdns36073:0crwdne36073:0"
|
||||
machines_number: "crwdns36075:0crwdne36075:0"
|
||||
edit: "crwdns36077:0crwdne36077:0"
|
||||
machine_category: "crwdns37393:0crwdne37393:0"
|
||||
machine_category_modal:
|
||||
new_machine_category: "crwdns36079:0crwdne36079:0"
|
||||
edit_machine_category: "crwdns36081:0crwdne36081:0"
|
||||
@ -40,12 +40,6 @@ zu:
|
||||
name: "crwdns36091:0crwdne36091:0"
|
||||
assigning_machines: "crwdns36093:0crwdne36093:0"
|
||||
save: "crwdns36095:0crwdne36095:0"
|
||||
delete_machine_category_modal:
|
||||
confirmation_required: "crwdns36097:0crwdne36097:0"
|
||||
confirm: "crwdns36099:0crwdne36099:0"
|
||||
deleted: "crwdns36101:0crwdne36101:0"
|
||||
unable_to_delete: "crwdns36103:0crwdne36103:0"
|
||||
confirm_machine_category: "crwdns36825:0crwdne36825:0"
|
||||
machine_form:
|
||||
ACTION_title: "crwdns36827:0ACTION={ACTION}crwdne36827:0"
|
||||
watch_out_when_creating_a_new_machine_its_prices_are_initialized_at_0_for_all_subscriptions: "crwdns36829:0crwdne36829:0"
|
||||
|
@ -166,7 +166,7 @@ de:
|
||||
end: "Expiry date"
|
||||
countdown: "Countdown"
|
||||
history: "History"
|
||||
consumed_hours: "H consumed"
|
||||
consumed_hours: "{COUNT, plural, =1{1H consumed} other{{COUNT}H consumed}}"
|
||||
cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack."
|
||||
select_machine: "Select a machine"
|
||||
cta_button: "Buy a pack"
|
||||
|
@ -166,7 +166,7 @@ en:
|
||||
end: "Expiry date"
|
||||
countdown: "Countdown"
|
||||
history: "History"
|
||||
consumed_hours: "H consumed"
|
||||
consumed_hours: "{COUNT, plural, =1{1H consumed} other{{COUNT}H consumed}}"
|
||||
cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack."
|
||||
select_machine: "Select a machine"
|
||||
cta_button: "Buy a pack"
|
||||
|
@ -166,7 +166,7 @@ es:
|
||||
end: "Expiry date"
|
||||
countdown: "Countdown"
|
||||
history: "History"
|
||||
consumed_hours: "H consumed"
|
||||
consumed_hours: "{COUNT, plural, =1{1H consumed} other{{COUNT}H consumed}}"
|
||||
cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack."
|
||||
select_machine: "Select a machine"
|
||||
cta_button: "Buy a pack"
|
||||
|
@ -166,7 +166,7 @@ fr:
|
||||
end: "Expire le"
|
||||
countdown: "Décompte"
|
||||
history: "Historique"
|
||||
consumed_hours: "H consommée(s)"
|
||||
consumed_hours: "{COUNT, plural, =1{1H consommée} other{{COUNT}H consommées}}"
|
||||
cta_info: "Vous pouvez acheter des packs d'heures prépayées pour réserver des machines et bénéficier de réductions. Choisissez une machine pour acheter un pack correspondant."
|
||||
select_machine: "Sélectionnez une machine"
|
||||
cta_button: "Acheter un pack"
|
||||
|
@ -166,7 +166,7 @@
|
||||
end: "Expiry date"
|
||||
countdown: "Countdown"
|
||||
history: "History"
|
||||
consumed_hours: "H consumed"
|
||||
consumed_hours: "{COUNT, plural, =1{1H consumed} other{{COUNT}H consumed}}"
|
||||
cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack."
|
||||
select_machine: "Select a machine"
|
||||
cta_button: "Buy a pack"
|
||||
|
@ -166,7 +166,7 @@ pt:
|
||||
end: "Expiry date"
|
||||
countdown: "Countdown"
|
||||
history: "History"
|
||||
consumed_hours: "H consumed"
|
||||
consumed_hours: "{COUNT, plural, =1{1H consumed} other{{COUNT}H consumed}}"
|
||||
cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack."
|
||||
select_machine: "Select a machine"
|
||||
cta_button: "Buy a pack"
|
||||
|
@ -166,7 +166,7 @@ zu:
|
||||
end: "crwdns37057:0crwdne37057:0"
|
||||
countdown: "crwdns37059:0crwdne37059:0"
|
||||
history: "crwdns37061:0crwdne37061:0"
|
||||
consumed_hours: "crwdns37063:0crwdne37063:0"
|
||||
consumed_hours: "crwdns37395:0COUNT={COUNT}crwdnd37395:0COUNT={COUNT}crwdne37395:0"
|
||||
cta_info: "crwdns37065:0crwdne37065:0"
|
||||
select_machine: "crwdns37067:0crwdne37067:0"
|
||||
cta_button: "crwdns37069:0crwdne37069:0"
|
||||
|
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# From this migration we save the association between a Reservation and a PrepaidPack to keep the
|
||||
# usage history.
|
||||
class CreatePrepaidPackReservations < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :prepaid_pack_reservations do |t|
|
||||
t.references :statistic_profile_prepaid_pack, foreign_key: true, index: { name: 'index_prepaid_pack_reservations_on_sp_prepaid_pack_id' }
|
||||
t.references :reservation, foreign_key: true
|
||||
t.integer :consumed_minutes
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
27
db/schema.rb
27
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_02_120458) do
|
||||
ActiveRecord::Schema.define(version: 2023_03_09_094535) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "fuzzystrmatch"
|
||||
@ -739,6 +739,17 @@ ActiveRecord::Schema.define(version: 2023_03_02_120458) do
|
||||
t.text "description"
|
||||
end
|
||||
|
||||
create_table "plan_limitations", force: :cascade do |t|
|
||||
t.bigint "plan_id"
|
||||
t.string "limitable_type"
|
||||
t.bigint "limitable_id"
|
||||
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"], name: "index_plan_limitations_on_plan_id"
|
||||
end
|
||||
|
||||
create_table "plans", id: :serial, force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.integer "amount"
|
||||
@ -758,6 +769,7 @@ ActiveRecord::Schema.define(version: 2023_03_02_120458) do
|
||||
t.boolean "disabled"
|
||||
t.boolean "monthly_payment"
|
||||
t.bigint "plan_category_id"
|
||||
t.boolean "limiting"
|
||||
t.index ["group_id"], name: "index_plans_on_group_id"
|
||||
t.index ["plan_category_id"], name: "index_plans_on_plan_category_id"
|
||||
end
|
||||
@ -769,6 +781,16 @@ ActiveRecord::Schema.define(version: 2023_03_02_120458) do
|
||||
t.index ["plan_id"], name: "index_plans_availabilities_on_plan_id"
|
||||
end
|
||||
|
||||
create_table "prepaid_pack_reservations", force: :cascade do |t|
|
||||
t.bigint "statistic_profile_prepaid_pack_id"
|
||||
t.bigint "reservation_id"
|
||||
t.integer "consumed_minutes"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["reservation_id"], name: "index_prepaid_pack_reservations_on_reservation_id"
|
||||
t.index ["statistic_profile_prepaid_pack_id"], name: "index_prepaid_pack_reservations_on_sp_prepaid_pack_id"
|
||||
end
|
||||
|
||||
create_table "prepaid_packs", force: :cascade do |t|
|
||||
t.string "priceable_type"
|
||||
t.bigint "priceable_id"
|
||||
@ -1422,7 +1444,10 @@ ActiveRecord::Schema.define(version: 2023_03_02_120458) do
|
||||
add_foreign_key "payment_schedules", "invoicing_profiles", column: "operator_profile_id"
|
||||
add_foreign_key "payment_schedules", "statistic_profiles"
|
||||
add_foreign_key "payment_schedules", "wallet_transactions"
|
||||
add_foreign_key "plan_limitations", "plans"
|
||||
add_foreign_key "plans", "plan_categories"
|
||||
add_foreign_key "prepaid_pack_reservations", "reservations"
|
||||
add_foreign_key "prepaid_pack_reservations", "statistic_profile_prepaid_packs"
|
||||
add_foreign_key "prepaid_packs", "groups"
|
||||
add_foreign_key "prices", "groups"
|
||||
add_foreign_key "prices", "plans"
|
||||
|
@ -294,15 +294,59 @@ namespace :fablab do
|
||||
Fablab::Application.load_tasks if Rake::Task.tasks.empty?
|
||||
Rake::Task['fablab:chain:invoices_items'].invoke
|
||||
end
|
||||
end
|
||||
|
||||
desc '[release 5.7] fix operator of self-bought carts'
|
||||
task cart_operator: :environment do |_task, _args|
|
||||
Order.where.not(statistic_profile_id: nil).find_each do |order|
|
||||
order.update(operator_profile_id: order.user&.invoicing_profile&.id)
|
||||
desc '[release 5.8.2] fix operator of self-bought carts'
|
||||
task cart_operator: :environment do |_task, _args|
|
||||
Order.where.not(statistic_profile_id: nil).find_each do |order|
|
||||
order.update(operator_profile_id: order.user&.invoicing_profile&.id)
|
||||
end
|
||||
Order.where.not(operator_profile_id: nil).find_each do |order|
|
||||
order.update(statistic_profile_id: order.operator_profile&.user&.statistic_profile&.id)
|
||||
end
|
||||
end
|
||||
Order.where.not(operator_profile_id: nil).find_each do |order|
|
||||
order.update(statistic_profile_id: order.operator_profile&.user&.statistic_profile&.id)
|
||||
|
||||
desc '[release 5.8.2] fix prepaid packs minutes_used'
|
||||
task pack_minutes_used: :environment do |_task, _args|
|
||||
StatisticProfilePrepaidPack.find_each do |sppp|
|
||||
previous_packs = sppp.statistic_profile.statistic_profile_prepaid_packs
|
||||
.includes(:prepaid_pack)
|
||||
.where(prepaid_packs: { priceable: sppp.prepaid_pack.priceable })
|
||||
.where("statistic_profile_prepaid_packs.created_at <= '#{sppp.created_at.utc.strftime('%Y-%m-%d %H:%M:%S.%6N')}'")
|
||||
.order('statistic_profile_prepaid_packs.created_at')
|
||||
remaining = {}
|
||||
previous_packs.each do |pack|
|
||||
available_minutes = pack.prepaid_pack.minutes
|
||||
reservations = Reservation.where(reservable: sppp.prepaid_pack.priceable)
|
||||
.where(statistic_profile_id: sppp.statistic_profile_id)
|
||||
.where("created_at > '#{pack.created_at.utc.strftime('%Y-%m-%d %H:%M:%S.%6N')}'")
|
||||
reservations.each do |reservation|
|
||||
next if available_minutes.zero?
|
||||
|
||||
# if the previous pack has not covered all the duration of this reservation, we substract the remaining minutes from the current pack
|
||||
if remaining[reservation.id]
|
||||
if remaining[reservation.id] > available_minutes
|
||||
consumed = available_minutes
|
||||
remaining[reservation.id] = remaining[reservation.id] - available_minutes
|
||||
else
|
||||
consumed = remaining[reservation.id]
|
||||
remaining.except!(reservation.id)
|
||||
end
|
||||
else
|
||||
# if there was no remaining from the previous pack, we substract the reservation duration from the current pack
|
||||
reservation_minutes = reservation.slots.map { |slot| (slot.end_at.to_time - slot.start_at.to_time) / 60.0 }.reduce(:+) || 0
|
||||
if reservation_minutes > available_minutes
|
||||
consumed = available_minutes
|
||||
remaining[reservation.id] = reservation_minutes - consumed
|
||||
else
|
||||
consumed = reservation_minutes
|
||||
end
|
||||
end
|
||||
available_minutes -= consumed
|
||||
PrepaidPackReservation.find_or_create_by!(statistic_profile_prepaid_pack: pack, reservation: reservation, consumed_minutes: consumed)
|
||||
end
|
||||
pack.update(minutes_used: pack.prepaid_pack.minutes - available_minutes)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fab-manager",
|
||||
"version": "5.8.1",
|
||||
"version": "5.8.2",
|
||||
"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",
|
||||
|
@ -6,8 +6,11 @@ parseparams()
|
||||
SCRIPTS=()
|
||||
ENVIRONMENTS=()
|
||||
PREPROCESSING=()
|
||||
while getopts "hyt:s:p:c:e:" opt; do
|
||||
while getopts "hyit:s:p:c:e:" opt; do
|
||||
case "${opt}" in
|
||||
i)
|
||||
IGNORE=true
|
||||
;;
|
||||
y)
|
||||
Y=true
|
||||
;;
|
||||
@ -173,20 +176,17 @@ version_check()
|
||||
VERSION=$(docker-compose exec -T "$SERVICE" cat package.json | jq -r '.version')
|
||||
fi
|
||||
target_version
|
||||
if [ "$TARGET" = 'custom' ]; then return; fi
|
||||
if [ "$TARGET" = 'custom' ] || [ "$IGNORE" = "true" ]; then return; fi
|
||||
|
||||
if verlt "$VERSION" 2.8.3 && verlt 2.8.3 "$TARGET"; then
|
||||
version_error "v2.8.3 first"
|
||||
elif verlt "$VERSION" 3.1.2 && verlt 3.1.2 "$TARGET"; then
|
||||
version_error "v3.1.2 first"
|
||||
elif verlt "$VERSION" 4.0.4 && verlt 4.0.4 "$TARGET"; then
|
||||
version_error "v4.0.4 first"
|
||||
elif verlt "$VERSION" 4.4.6 && verlt 4.4.6 "$TARGET"; then
|
||||
version_error "v4.4.6 first"
|
||||
elif verlt "$VERSION" 4.7.14 && verlt 4.7.14 "$TARGET"; then
|
||||
version_error "v4.7.14 first"
|
||||
elif verlt "$TARGET" "$VERSION"; then
|
||||
version_error "a version > $VERSION"
|
||||
HTTP_CODE=$(curl -I -s -w "%{http_code}\n" -o /dev/null "https://hub.fab-manager.com/api/versions/next_step?version=$VERSION")
|
||||
if [ "$HTTP_CODE" != 200 ]; then
|
||||
printf "\n\n\e[91m[ ❌ ] Unable to check the next step version. Please check your internet connection or restart this script providing the \e[1m-i\e[0m\e[91m option\n\e[39m"
|
||||
exit 3
|
||||
fi
|
||||
STEP=$(\curl -sSL "https://hub.fab-manager.com/api/versions/next_step?version=$VERSION" | jq -r '.next_step.semver')
|
||||
|
||||
if verlt "$VERSION" "$STEP" && verlt "$STEP" "$TARGET"; then
|
||||
version_error "$STEP first"
|
||||
fi
|
||||
}
|
||||
|
||||
@ -262,7 +262,10 @@ restore_tag()
|
||||
|
||||
upgrade()
|
||||
{
|
||||
[[ "$YES_ALL" = "true" ]] && confirm="y" || read -rp "[91m::[0m [1mProceed with upgrading to version $TARGET ?[0m (Y/n) " confirm </dev/tty
|
||||
local user_target="$TARGET"
|
||||
if [ "$TARGET" = 'custom' ]; then user_target="$TAG"; fi
|
||||
|
||||
[[ "$YES_ALL" = "true" ]] && confirm="y" || read -rp "[91m::[0m [1mProceed with upgrading to version $user_target ?[0m (Y/n) " confirm </dev/tty
|
||||
if [[ "$confirm" = "n" ]]; then exit 2; fi
|
||||
|
||||
add_environments
|
||||
@ -311,7 +314,6 @@ upgrade()
|
||||
fi
|
||||
done
|
||||
docker-compose up -d
|
||||
restore_tag
|
||||
docker ps
|
||||
}
|
||||
|
||||
@ -331,6 +333,7 @@ usage()
|
||||
Options:
|
||||
-h Print this message and quit
|
||||
-y Answer yes to all questions
|
||||
-i Ignore the target version check
|
||||
-t <string> Force the upgrade to target the specified version
|
||||
-p <string> Run the preprocessing command (TODO DEPLOY)
|
||||
-c <string> Provides additional upgrade command, run in the context of the app (TODO DEPLOY)
|
||||
|
8
test/fixtures/coupons.yml
vendored
8
test/fixtures/coupons.yml
vendored
@ -47,3 +47,11 @@ twentyp:
|
||||
updated_at: '2021-06-18 14:53:54.770895'
|
||||
validity_per_user: forever
|
||||
amount_off:
|
||||
internal:
|
||||
name: Internal use
|
||||
code: INTERNCOUP100
|
||||
percent_off: 100
|
||||
valid_until:
|
||||
max_usages:
|
||||
active: true
|
||||
validity_per_user: forever
|
||||
|
@ -46,6 +46,19 @@ class OpenApi::ReservationsTest < ActionDispatch::IntegrationTest
|
||||
assert_equal [3], reservations[:reservations].pluck(:user_id).uniq
|
||||
end
|
||||
|
||||
test 'list all reservations with dates filtering' do
|
||||
get '/open_api/v1/reservations?after=2012-01-01T00:00:00+02:00&before=2012-12-31T23:59:59+02:00', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
assert_equal Mime[:json], response.content_type
|
||||
|
||||
reservations = json_response(response.body)
|
||||
assert reservations[:reservations].count.positive?
|
||||
assert(reservations[:reservations].all? do |line|
|
||||
date = Time.zone.parse(line[:created_at])
|
||||
date >= '2012-01-01'.to_date && date <= '2012-12-31'.to_date
|
||||
end)
|
||||
end
|
||||
|
||||
test 'list all machine reservations for a user' do
|
||||
get '/open_api/v1/reservations?reservable_type=Machine&user_id=3', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
|
@ -20,20 +20,38 @@ class OpenApi::SubscriptionsTest < ActionDispatch::IntegrationTest
|
||||
test 'list subscriptions with pagination' do
|
||||
get '/open_api/v1/subscriptions?page=1&per_page=5', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
assert_equal Mime[:json], response.content_type
|
||||
|
||||
subscriptions = json_response(response.body)
|
||||
assert subscriptions[:subscriptions].count <= 5
|
||||
end
|
||||
|
||||
test 'list all subscriptions for a user' do
|
||||
get '/open_api/v1/subscriptions?user_id=3', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
|
||||
subscriptions = json_response(response.body)
|
||||
assert_not_empty subscriptions[:subscriptions]
|
||||
assert_equal [3], subscriptions[:subscriptions].pluck(:user_id).uniq
|
||||
end
|
||||
|
||||
test 'list all subscriptions for a user with pagination' do
|
||||
get '/open_api/v1/subscriptions?user_id=3&page=1&per_page=5', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
|
||||
subscriptions = json_response(response.body)
|
||||
assert_not_empty subscriptions[:subscriptions]
|
||||
assert_equal [3], subscriptions[:subscriptions].pluck(:user_id).uniq
|
||||
assert subscriptions[:subscriptions].count <= 5
|
||||
end
|
||||
|
||||
test 'list all subscriptions for a plan with pagination' do
|
||||
get '/open_api/v1/subscriptions?plan_id=1&page=1&per_page=5', headers: open_api_headers(@token)
|
||||
assert_response :success
|
||||
|
||||
subscriptions = json_response(response.body)
|
||||
assert_not_empty subscriptions[:subscriptions]
|
||||
assert_equal [1], subscriptions[:subscriptions].pluck(:plan_id).uniq
|
||||
assert subscriptions[:subscriptions].count <= 5
|
||||
end
|
||||
end
|
||||
|
@ -19,10 +19,14 @@ class OpenApi::UsersTest < ActionDispatch::IntegrationTest
|
||||
assert_not_nil(users[:users].detect { |u| u[:external_id] == 'J5821-4' })
|
||||
assert(users[:users].all? { |u| %w[man woman].include?(u[:gender]) })
|
||||
assert(users[:users].all? { |u| u[:organization] != User.find(u[:id]).invoicing_profile.organization.nil? })
|
||||
assert(users[:users].all? { |u| u[:invoicing_profile_id].present? })
|
||||
assert(users[:users].all? { |u| u[:full_name].present? })
|
||||
assert(users[:users].all? { |u| u[:first_name].present? })
|
||||
assert(users[:users].all? { |u| u[:last_name].present? })
|
||||
assert(users[:users].any? { |u| u[:address].present? })
|
||||
assert(users[:users].all? { |u| u[:group][:id] == User.find(u[:id]).group_id })
|
||||
assert(users[:users].all? { |u| u[:group][:name].present? })
|
||||
assert(users[:users].all? { |u| u[:group][:slug].present? })
|
||||
end
|
||||
|
||||
test 'list all users with pagination' do
|
||||
|
@ -206,4 +206,51 @@ class Store::AdminOrderForHimselfTest < ActionDispatch::IntegrationTest
|
||||
|
||||
assert_equal 403, response.status
|
||||
end
|
||||
|
||||
test 'admin pay a free order with success' do
|
||||
login_as(@admin, scope: :user)
|
||||
|
||||
invoice_count = Invoice.count
|
||||
invoice_items_count = InvoiceItem.count
|
||||
|
||||
post '/api/checkout/payment',
|
||||
params: {
|
||||
coupon_code: 'INTERNCOUP100',
|
||||
order_token: @cart1.token,
|
||||
customer_id: @admin.id
|
||||
}.to_json, headers: default_headers
|
||||
|
||||
@cart1.reload
|
||||
|
||||
# general assertions
|
||||
assert_equal 200, response.status
|
||||
assert_equal invoice_count + 1, Invoice.count
|
||||
assert_equal invoice_items_count + 2, InvoiceItem.count
|
||||
|
||||
# invoice_items assertions
|
||||
invoice_item = InvoiceItem.last
|
||||
|
||||
assert invoice_item.check_footprint
|
||||
|
||||
# invoice assertions
|
||||
invoice = Invoice.last
|
||||
assert_invoice_pdf invoice
|
||||
assert_not_nil invoice.debug_footprint
|
||||
|
||||
assert @cart1.payment_gateway_object.blank?
|
||||
assert invoice.payment_gateway_object.blank?
|
||||
assert invoice.total.zero?
|
||||
assert invoice.check_footprint
|
||||
|
||||
# notification
|
||||
assert_not_empty Notification.where(attached_object: invoice)
|
||||
|
||||
assert_equal 'paid', @cart1.state
|
||||
assert_equal 'local', @cart1.payment_method
|
||||
assert_equal 0, @cart1.paid_total
|
||||
|
||||
activity = @cart1.order_activities.last
|
||||
assert_equal 'paid', activity.activity_type
|
||||
assert_equal @admin.invoicing_profile.id, activity.operator_profile_id
|
||||
end
|
||||
end
|
||||
|
@ -3,7 +3,7 @@
|
||||
require 'test_helper'
|
||||
|
||||
class StatisticProfilePrepaidPackTest < ActiveSupport::TestCase
|
||||
test 'coupon have a expries date' do
|
||||
test 'prepaid pack have a expries date' do
|
||||
prepaid_pack = PrepaidPack.first
|
||||
user = User.find_by(username: 'jdupond')
|
||||
p = StatisticProfilePrepaidPack.create!(prepaid_pack: prepaid_pack, statistic_profile: user.statistic_profile)
|
||||
|
@ -17,8 +17,16 @@ class AccountingServiceTest < ActionDispatch::IntegrationTest
|
||||
|
||||
# enable the VAT
|
||||
Setting.set('invoice_VAT-active', true)
|
||||
Setting.set('invoice_VAT-rate', 19.6)
|
||||
Setting.set('invoice_VAT-rate', '19.6')
|
||||
|
||||
# enable advanced accounting on the plan
|
||||
Setting.set('advanced_accounting', true)
|
||||
plan.update(advanced_accounting_attributes: {
|
||||
code: '7021',
|
||||
analytical_section: 'PL21'
|
||||
})
|
||||
|
||||
# book and pay
|
||||
post '/api/local_payment/confirm_payment', params: {
|
||||
customer_id: @vlonchamp.id,
|
||||
coupon_code: 'GIME3EUR',
|
||||
@ -73,11 +81,13 @@ class AccountingServiceTest < ActionDispatch::IntegrationTest
|
||||
assert_not_nil item_machine
|
||||
assert_equal invoice.main_item.net_amount, item_machine&.credit
|
||||
assert_equal Setting.get('accounting_sales_journal_code'), item_machine&.journal_code
|
||||
|
||||
# Check the subscription line
|
||||
item_suscription = lines.find { |l| l.account_code == Setting.get('accounting_subscription_code') }
|
||||
item_suscription = lines.find { |l| l.account_code == '7021' }
|
||||
assert_not_nil item_suscription
|
||||
assert_equal invoice.other_items.last.net_amount, item_suscription&.credit
|
||||
assert_equal Setting.get('accounting_sales_journal_code'), item_suscription&.journal_code
|
||||
assert_equal '7021', item_suscription&.account_code
|
||||
|
||||
# Check the VAT line
|
||||
vat_service = VatHistoryService.new
|
||||
|
@ -37,4 +37,40 @@ class PrepaidPackServiceTest < ActiveSupport::TestCase
|
||||
minutes_available = PrepaidPackService.minutes_available(@acamus, @machine)
|
||||
assert_equal minutes_available, 480
|
||||
end
|
||||
|
||||
test 'member has multiple active packs' do
|
||||
availabilities_service = Availabilities::AvailabilitiesService.new(@acamus)
|
||||
|
||||
# user with current pack reserve 8 slots (on 10 available in the pack)
|
||||
slots = availabilities_service.machines([@machine], @acamus, { start: Time.current, end: 10.days.from_now })
|
||||
reservation = Reservation.create(
|
||||
reservable_id: @machine.id,
|
||||
reservable_type: Machine.name,
|
||||
slots: [slots[0], slots[1], slots[2], slots[3], slots[4], slots[5], slots[6], slots[7]],
|
||||
statistic_profile_id: @acamus.statistic_profile.id
|
||||
)
|
||||
|
||||
PrepaidPackService.update_user_minutes(@acamus, reservation)
|
||||
minutes_available = PrepaidPackService.minutes_available(@acamus, @machine)
|
||||
assert_equal 120, minutes_available
|
||||
|
||||
# user buy a new pack
|
||||
prepaid_pack = PrepaidPack.first
|
||||
StatisticProfilePrepaidPack.create!(prepaid_pack: prepaid_pack, statistic_profile: @acamus.statistic_profile)
|
||||
|
||||
minutes_available = PrepaidPackService.minutes_available(@acamus, @machine)
|
||||
assert_equal 720, minutes_available
|
||||
|
||||
# user books a new reservation of 4 slots
|
||||
reservation = Reservation.create(
|
||||
reservable_id: @machine.id,
|
||||
reservable_type: Machine.name,
|
||||
slots: [slots[8], slots[9], slots[10], slots[11]],
|
||||
statistic_profile_id: @acamus.statistic_profile.id
|
||||
)
|
||||
|
||||
PrepaidPackService.update_user_minutes(@acamus, reservation)
|
||||
minutes_available = PrepaidPackService.minutes_available(@acamus, @machine)
|
||||
assert_equal 480, minutes_available
|
||||
end
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user