1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-06 01:08:21 +01:00

Merge branch 'dev' for release 2.4.7

This commit is contained in:
Sylvain 2016-12-14 10:41:46 +01:00
commit 96259ab257
33 changed files with 475 additions and 101 deletions

View File

@ -1 +1 @@
2.4.6 2.4.7

4
.gitignore vendored
View File

@ -15,6 +15,7 @@
# Ignore all logfiles and tempfiles. # Ignore all logfiles and tempfiles.
/log/*.log /log/*.log
/tmp /tmp
/coverage
/public/uploads /public/uploads
/public/assets /public/assets
@ -39,5 +40,8 @@
.vagrant .vagrant
.docker .docker
# Do not versionate coveralls token
.coveralls.yml
# Plugins are versioned is their own repository # Plugins are versioned is their own repository
/plugins/* /plugins/*

View File

@ -1,9 +1,18 @@
# Changelog Fab Manager # Changelog Fab Manager
## v2.4.6 2016 Novembre 30 ## v2.4.7 2016 December 14
- Improved automated testing
- Added an information notice about the processing time of deleting an administrator
- Ability to change the expiration date of a coupon after its creation
- Ability to generate a refund invoice when crediting user's wallet
- Fix a bug: unable to run rake db:migrate on first install
- Fix a bug: unable to create or edit a coupon of type 'percentage'
## v2.4.6 2016 November 30
- Change display of message about coupon application status - Change display of message about coupon application status
- Fix a bug: compute price API return error 500 if reservable_id is not provided - Fix a bug: compute price API return error 500 if reservable_id is not provided
## v2.4.5 2016 November 29 ## v2.4.5 2016 November 29
@ -25,8 +34,8 @@
## v2.4.3 2016 November 21 ## v2.4.3 2016 November 21
- Export user's invoicing status in members' excel export - Export user's invoicing status in members' excel export
- Fix a bug: Next events descriptions, shown on the home page, display raw html - Fix a bug: Next events descriptions, shown on the home page, display raw html
- Fix a bug: number of reserved seats for an event is always of 1 in the excel export of reservations - Fix a bug: number of reserved seats for an event is always of 1 in the excel export of reservations
- Fix a bug: conflict between similar translations around "reservations" - Fix a bug: conflict between similar translations around "reservations"
- Fix a bug: later occurrences of recurrent events does not have the initially configured theme and age range - Fix a bug: later occurrences of recurrent events does not have the initially configured theme and age range
- Fix a bug: some graphs do not display: events, users, trainings and machine hours - Fix a bug: some graphs do not display: events, users, trainings and machine hours
@ -44,18 +53,18 @@
## v2.4.1 2016 October 11 ## v2.4.1 2016 October 11
- Fix a bug: unable to share a project/event without image on social networks - Fix a bug: unable to share a project/event without image on social networks
- Fix a bug: after creating an element in the admin calendar, browsing through the calendar and coming back cause the element to appear duplicated - Fix a bug: after creating an element in the admin calendar, browsing through the calendar and coming back cause the element to appear duplicated
- Fix a bug: after deleting an element in the admin calendar, the confirmation message is wrong and an error is logged in the console - Fix a bug: after deleting an element in the admin calendar, the confirmation message is wrong and an error is logged in the console
- Fix a bug: erroneous syntax in docker env example file - Fix a bug: erroneous syntax in docker env example file
## v2.4.0 2016 October 4 ## v2.4.0 2016 October 4
- RSS feeds to follow new projects and events published - RSS feeds to follow new projects and events published
- Use slugs in projects URL opened from notifications - Use slugs in projects URL opened from notifications
- Ask for confirmation on machine deletion from the public view - Ask for confirmation on machine deletion from the public view
- Ability to delete a training from the public view for an admin - Ability to delete a training from the public view for an admin
- Project images will show in full-size on a click - Project images will show in full-size on a click
- Add a checkbox "I accept to receive informations from the FabLab" on Sign-up dialog and user's profile - Add a checkbox "I accept to receive informations from the FabLab" on Sign-up dialog and user's profile
- Share project with Facebook/Twitter - Share project with Facebook/Twitter
- Display fab-manager's version in "Powered by" label, when logged as admin - Display fab-manager's version in "Powered by" label, when logged as admin
@ -70,14 +79,14 @@
- Trainings are associated with a picture and an HTML textual description - Trainings are associated with a picture and an HTML textual description
- Public gallery of trainings with ability to view details or to book a training on its own calendar - Public gallery of trainings with ability to view details or to book a training on its own calendar
- Ability to switch back to all trainings booking view - Ability to switch back to all trainings booking view
- Rename "Courses and Workshops" to "Events" - Rename "Courses and Workshops" to "Events"
- Admin: Events can be associated with a theme and an age range - Admin: Events can be associated with a theme and an age range
- Admin: Event categories, themes and age ranges can be customized - Admin: Event categories, themes and age ranges can be customized
- Filter events by category, theme and age range in public view - Filter events by category, theme and age range in public view
- Ability to customise price's categories for the events - Ability to customise price's categories for the events
- Events can be associated with many custom price's categories, instead of only one "reduced price" - Events can be associated with many custom price's categories, instead of only one "reduced price"
- Statistics views can trigger and display custom aggregations from ElasticSearch - Statistics views can trigger and display custom aggregations from ElasticSearch
- Machine hours/Trainings statistics: display number of tickets/hours available for booking - Machine hours/Trainings statistics: display number of tickets/hours available for booking
- Statistics will include informations abouts events category, theme and age range - Statistics will include informations abouts events category, theme and age range
- Ability to export the current statistics table to an Excel file - Ability to export the current statistics table to an Excel file
- Ability to export every statistics on a given dates range to an Excel file - Ability to export every statistics on a given dates range to an Excel file
@ -93,16 +102,16 @@
- More file types allowed as project CAD attachements - More file types allowed as project CAD attachements
- Project CAD attachements are now checked by MIME type in addition of extension check - Project CAD attachements are now checked by MIME type in addition of extension check
- Project CAD attachement allowed are now configured in environment variables - Project CAD attachement allowed are now configured in environment variables
- Project CAD attachement extensions allowed are shown next to input field - Project CAD attachement extensions allowed are shown next to input field
- Display strategy's name in SSO providers list - Display strategy's name in SSO providers list
- SSO: documentation improved with an usage example - SSO: documentation improved with an usage example
- SSO: mapped fields display their data type. Integers, booleans and dates allow some transformations. - SSO: mapped fields display their data type. Integers, booleans and dates allow some transformations.
- Fix a bug: project drafts are shown on public profiles - Fix a bug: project drafts are shown on public profiles
- Fix a bug: event category disappear when editing the event - Fix a bug: event category disappear when editing the event
- Fix a bug: machine name is not shown in plan edition - Fix a bug: machine name is not shown in plan edition
- Fix a bug: machine slots with tags are not displayed correctly on reservation calendar - Fix a bug: machine slots with tags are not displayed correctly on reservation calendar
- Fix a bug: avatar, address and organization details mapping from SSO were broken - Fix a bug: avatar, address and organization details mapping from SSO were broken
- Fix a bug: in SSO configuration some valid endpoints were recognized as erroneous - Fix a bug: in SSO configuration some valid endpoints were recognized as erroneous
- Fix a bug: clicking on the text in stripe's payment modal, does not validate the checkbox - Fix a bug: clicking on the text in stripe's payment modal, does not validate the checkbox
- Fix a bug: move event reservation is not limited by admin settings (prior-delay & disable) - Fix a bug: move event reservation is not limited by admin settings (prior-delay & disable)
- Fix a bug: UI issues on small devices (dashboard + admin views) - Fix a bug: UI issues on small devices (dashboard + admin views)
@ -134,7 +143,7 @@
- [TODO DEPLOY] `bundle install` and `rake db:migrate` - [TODO DEPLOY] `bundle install` and `rake db:migrate`
## v2.2.2 2016 June 23 ## v2.2.2 2016 June 23
- Fix some bugs: users with uncompleted account (sso imported) won't appear in statistics, in listings and in searches. Moreover, they won't block statistics generation - Fix some bugs: users with uncompleted account (sso imported) won't appear in statistics, in listings and in searches. Moreover, they won't block statistics generation
- Fix a bug: unable to display next results in statistics tables - Fix a bug: unable to display next results in statistics tables
- Admin: Category is mandatory when creating an event - Admin: Category is mandatory when creating an event
@ -150,7 +159,7 @@
- User public profile: UI re-design with possible admin's customization - User public profile: UI re-design with possible admin's customization
- Admin: Invoices list and users list are now loaded per 10 items to improve pages load time - Admin: Invoices list and users list are now loaded per 10 items to improve pages load time
- Admin: select member (eg. to buy a subscription for a member) is now loading the user's list dynamically when you type - Admin: select member (eg. to buy a subscription for a member) is now loading the user's list dynamically when you type
- Project collaborators selection is now using a list dynamically loaded as you type - Project collaborators selection is now using a list dynamically loaded as you type
- Admin: select a training before monitoring its reservations -> improves page load time - Admin: select a training before monitoring its reservations -> improves page load time
- API: GET /api/trainings do not load nor send the associated availabilities until they are requested - API: GET /api/trainings do not load nor send the associated availabilities until they are requested
- List of members is now loaded 10 members by 10, to improve page load time - List of members is now loaded 10 members by 10, to improve page load time

View File

@ -52,6 +52,8 @@ group :development do
gem 'capistrano-maintenance', '0.0.5', require: false gem 'capistrano-maintenance', '0.0.5', require: false
gem 'active_record_query_trace' gem 'active_record_query_trace'
gem 'coveralls', require: false
end end
group :test do group :test do
@ -62,6 +64,7 @@ group :test do
gem 'webmock' gem 'webmock'
gem 'vcr' gem 'vcr'
gem 'byebug' gem 'byebug'
gem 'pdf-reader'
end end
group :production do group :production do

View File

@ -1,6 +1,7 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
Ascii85 (1.0.2)
aasm (4.1.0) aasm (4.1.0)
actionmailer (4.2.5) actionmailer (4.2.5)
actionpack (= 4.2.5) actionpack (= 4.2.5)
@ -41,6 +42,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
addressable (2.3.8) addressable (2.3.8)
afm (0.2.2)
ansi (1.5.0) ansi (1.5.0)
api-pagination (4.3.0) api-pagination (4.3.0)
apipie-rails (0.3.6) apipie-rails (0.3.6)
@ -118,6 +120,12 @@ GEM
sass-rails (<= 5.0.1) sass-rails (<= 5.0.1)
sprockets (< 2.13) sprockets (< 2.13)
connection_pool (2.2.0) connection_pool (2.2.0)
coveralls (0.8.16)
json (>= 1.8, < 3)
simplecov (~> 0.12.0)
term-ansicolor (~> 1.3.0)
thor (~> 0.19.1)
tins (>= 1.6.0, < 2)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
database_cleaner (1.4.1) database_cleaner (1.4.1)
@ -133,6 +141,7 @@ GEM
warden (~> 1.2.3) warden (~> 1.2.3)
devise-async (0.9.0) devise-async (0.9.0)
devise (~> 3.2) devise (~> 3.2)
docile (1.1.5)
domain_name (0.5.25) domain_name (0.5.25)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
elasticsearch (1.0.12) elasticsearch (1.0.12)
@ -177,6 +186,7 @@ GEM
has_secure_token (1.0.0) has_secure_token (1.0.0)
activerecord (>= 3.0) activerecord (>= 3.0)
hashdiff (0.3.0) hashdiff (0.3.0)
hashery (2.1.2)
hashie (3.4.2) hashie (3.4.2)
highline (1.7.1) highline (1.7.1)
hike (1.2.3) hike (1.2.3)
@ -265,6 +275,12 @@ GEM
httparty (~> 0.13) httparty (~> 0.13)
orm_adapter (0.5.0) orm_adapter (0.5.0)
pdf-core (0.5.1) pdf-core (0.5.1)
pdf-reader (1.4.0)
Ascii85 (~> 1.0.0)
afm (~> 0.2.1)
hashery (~> 2.0)
ruby-rc4
ttfunk
pg (0.18.1) pg (0.18.1)
pkg-config (1.1.7) pkg-config (1.1.7)
prawn (2.0.1) prawn (2.0.1)
@ -335,6 +351,7 @@ GEM
netrc (~> 0.7) netrc (~> 0.7)
rolify (4.0.0) rolify (4.0.0)
ruby-progressbar (1.7.5) ruby-progressbar (1.7.5)
ruby-rc4 (0.1.5)
rubyzip (1.1.7) rubyzip (1.1.7)
rufus-scheduler (3.0.9) rufus-scheduler (3.0.9)
tzinfo tzinfo
@ -365,6 +382,11 @@ GEM
sidekiq (>= 2.17.3) sidekiq (>= 2.17.3)
tilt (< 2.0.0) tilt (< 2.0.0)
simple_oauth (0.3.1) simple_oauth (0.3.1)
simplecov (0.12.0)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
sinatra (1.4.6) sinatra (1.4.6)
rack (~> 1.4) rack (~> 1.4)
rack-protection (~> 1.4) rack-protection (~> 1.4)
@ -382,6 +404,8 @@ GEM
stripe (1.30.2) stripe (1.30.2)
json (~> 1.8.1) json (~> 1.8.1)
rest-client (~> 1.4) rest-client (~> 1.4)
term-ansicolor (1.3.2)
tins (~> 1.0)
test_after_commit (1.0.0) test_after_commit (1.0.0)
activerecord (>= 3.2) activerecord (>= 3.2)
therubyracer (0.12.0) therubyracer (0.12.0)
@ -392,6 +416,7 @@ GEM
tilt (1.4.1) tilt (1.4.1)
timers (4.0.1) timers (4.0.1)
hitimes hitimes
tins (1.13.0)
ttfunk (1.4.0) ttfunk (1.4.0)
twitter (5.14.0) twitter (5.14.0)
addressable (~> 2.3) addressable (~> 2.3)
@ -462,6 +487,7 @@ DEPENDENCIES
chroma chroma
coffee-rails (~> 4.1.0) coffee-rails (~> 4.1.0)
compass-rails (= 2.0.4) compass-rails (= 2.0.4)
coveralls
database_cleaner database_cleaner
devise devise
devise-async devise-async
@ -488,6 +514,7 @@ DEPENDENCIES
omniauth omniauth
omniauth-oauth2 omniauth-oauth2
openlab_ruby openlab_ruby
pdf-reader
pg pg
prawn prawn
prawn-table prawn-table
@ -522,4 +549,4 @@ DEPENDENCIES
webmock webmock
BUNDLED WITH BUNDLED WITH
1.12.5 1.13.1

View File

@ -58,8 +58,8 @@ Application.Controllers.controller "NewCouponController", ["$scope", "$state", '
## ##
# Controller used in the coupon edition page # Controller used in the coupon edition page
## ##
Application.Controllers.controller "EditCouponController", ["$scope", "$state", 'Coupon', 'couponPromise', '_t' Application.Controllers.controller "EditCouponController", ["$scope", "$state", 'Coupon', 'couponPromise', '_t', 'growl'
, ($scope, $state, Coupon, couponPromise, _t) -> , ($scope, $state, Coupon, couponPromise, _t, growl) ->
### PUBLIC SCOPE ### ### PUBLIC SCOPE ###
@ -73,6 +73,9 @@ Application.Controllers.controller "EditCouponController", ["$scope", "$state",
## Options for the validity per user ## Options for the validity per user
$scope.validities = userValidities $scope.validities = userValidities
## Mapping for validation errors
$scope.errors = {}
## Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection) ## Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection)
$scope.datePicker = $scope.datePicker =
format: Fablab.uibDateFormat format: Fablab.uibDateFormat
@ -98,11 +101,12 @@ Application.Controllers.controller "EditCouponController", ["$scope", "$state",
# Callback to save the coupon's changes to the API # Callback to save the coupon's changes to the API
## ##
$scope.updateCoupon = -> $scope.updateCoupon = ->
$scope.errors = {}
Coupon.update {id: $scope.coupon.id}, coupon: $scope.coupon, (coupon) -> Coupon.update {id: $scope.coupon.id}, coupon: $scope.coupon, (coupon) ->
$state.go('app.admin.pricing') $state.go('app.admin.pricing')
, (err)-> , (err)->
growl.error(_t('unable_to_update_the_coupon_an_error_occurred')) growl.error(_t('unable_to_update_the_coupon_an_error_occurred'))
console.error(err) $scope.errors = err.data

View File

@ -105,8 +105,8 @@ class MembersController
## ##
# Controller used in the members/groups management page # Controller used in the members/groups management page
## ##
Application.Controllers.controller "AdminMembersController", ["$scope", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export' Application.Controllers.controller "AdminMembersController", ["$scope","$sce", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export'
, ($scope, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export) -> , ($scope, $sce, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export) ->
@ -177,7 +177,7 @@ Application.Controllers.controller "AdminMembersController", ["$scope", 'members
resolve: resolve:
object: -> object: ->
title: _t('confirmation_required') title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_administrator_this_cannot_be_undone') msg: $sce.trustAsHtml(_t('do_you_really_want_to_delete_this_administrator_this_cannot_be_undone') + '<br/><br/>' +_t('this_may_take_a_while_please_wait'))
, -> # cancel confirmed , -> # cancel confirmed
Admin.delete id: admin.id, -> Admin.delete id: admin.id, ->
admins.splice(findAdminIdxById(admins, admin.id), 1) admins.splice(findAdminIdxById(admins, admin.id), 1)
@ -423,11 +423,40 @@ Application.Controllers.controller "EditMemberController", ["$scope", "$state",
templateUrl: '<%= asset_path "wallet/credit_modal.html" %>' templateUrl: '<%= asset_path "wallet/credit_modal.html" %>'
controller: ['$scope', '$uibModalInstance', 'Wallet', ($scope, $uibModalInstance, Wallet) -> controller: ['$scope', '$uibModalInstance', 'Wallet', ($scope, $uibModalInstance, Wallet) ->
# default: do not generate a refund invoice
$scope.generate_avoir = false
# date of the generated refund invoice
$scope.avoir_date = null
# optional description shown on the refund invoice
$scope.description = ''
# default configuration for the avoir date selector widget
$scope.datePicker =
format: Fablab.uibDateFormat
opened: false
options:
startingDay: Fablab.weekStartingDay
##
# Callback to open/close the date picker
##
$scope.toggleDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.opened = !$scope.datePicker.opened
## ##
# Modal dialog validation callback # Modal dialog validation callback
## ##
$scope.ok = -> $scope.ok = ->
Wallet.credit { id: wallet.id }, { amount: $scope.amount }, (_wallet)-> Wallet.credit { id: wallet.id },
amount: $scope.amount
avoir: $scope.generate_avoir
avoir_date: $scope.avoir_date
avoir_description: $scope.description
, (_wallet)->
growl.success(_t('wallet_credit_successfully')) growl.success(_t('wallet_credit_successfully'))
$uibModalInstance.close(_wallet) $uibModalInstance.close(_wallet)

View File

@ -81,7 +81,7 @@
<span class="help-block error" ng-show="couponForm['coupon[validity_per_user]'].$dirty && couponForm['coupon[validity_per_user]'].$error.required" translate>{{ 'validity_per_user_is_required' }}</span> <span class="help-block error" ng-show="couponForm['coupon[validity_per_user]'].$dirty && couponForm['coupon[validity_per_user]'].$error.required" translate>{{ 'validity_per_user_is_required' }}</span>
</div> </div>
<div class="form-group"> <div class="form-group" ng-class="{'has-error': errors['valid_until']}">
<label for="coupon[valid_until]" translate>{{ 'valid_until' }}</label> <label for="coupon[valid_until]" translate>{{ 'valid_until' }}</label>
<div class="input-group"> <div class="input-group">
<input type="text" id="coupon[valid_until]" <input type="text" id="coupon[valid_until]"
@ -92,16 +92,16 @@
datepicker-options="datePicker.options" datepicker-options="datePicker.options"
is-open="datePicker.opened" is-open="datePicker.opened"
min-date="datePicker.minDate" min-date="datePicker.minDate"
ng-disabled="mode == 'EDIT'"
ng-click="toggleDatePicker($event)"/> ng-click="toggleDatePicker($event)"/>
<span class="input-group-btn"> <span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="toggleDatePicker($event)" ng-disabled="mode == 'EDIT'"><i class="fa fa-calendar"></i></button> <button type="button" class="btn btn-default" ng-click="toggleDatePicker($event)" ng-disabled="mode == 'EDIT'"><i class="fa fa-calendar"></i></button>
</span> </span>
</div> </div>
<span class="help-block error" ng-show="errors['valid_until']">{{ errors['valid_until'].join(' ; ') }}</span>
<span class="help-block text-info text-xs"> <span class="text-info text-xs">
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }} <i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
</span> </span>
</div> </div>
<div class="form-group" ng-class="{'has-error': couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$invalid}"> <div class="form-group" ng-class="{'has-error': couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$invalid}">
@ -114,7 +114,7 @@
min="0"/> min="0"/>
<span class="help-block error" ng-show="couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$error.min" translate>{{ 'max_usages_must_be_equal_or_greater_than_0' }}</span> <span class="help-block error" ng-show="couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$error.min" translate>{{ 'max_usages_must_be_equal_or_greater_than_0' }}</span>
<span class="help-block text-info text-xs"> <span class="text-info text-xs">
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }} <i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
</span> </span>
</div> </div>

View File

@ -3,7 +3,7 @@
<h1>{{object.title}}</h1> <h1>{{object.title}}</h1>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>{{object.msg}}</p> <p ng-bind-html="object.msg"></p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-info" ng-click="ok()" translate>{{ 'confirm' }}</button> <button class="btn btn-info" ng-click="ok()" translate>{{ 'confirm' }}</button>

View File

@ -3,11 +3,18 @@
<h1 translate>{{ 'credit_title' }}</h1> <h1 translate>{{ 'credit_title' }}</h1>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="alert alert-warning m-b-md m-b-sm">
<i class="fa fa-warning m-sm inline" aria-hidden="true"></i>
<div class="inline pull-right width-90 m-t-n-xs" translate>{{ 'warning_uneditable_credit' }}</div>
</div>
<form name="walletForm" ng-class="{'has-error': walletForm.amount.$dirty && walletForm.amount.$invalid}"> <form name="walletForm" ng-class="{'has-error': walletForm.amount.$dirty && walletForm.amount.$invalid}">
<div class="text-right amountGroup m-r-md"> <div class="text-right amountGroup m-r-md">
<span class="beforeAmount" translate>{{ 'credit_label' }}</span> <label for="amount" class="beforeAmount" translate>{{ 'credit_label' }}</label>
<input class="form-control m-l" <input class="form-control m-l"
type="number" type="number"
id="amount"
name="amount" name="amount"
ng-model="amount" ng-model="amount"
required min="1" required min="1"
@ -16,10 +23,11 @@
<span class="help-block" ng-show="walletForm.amount.$dirty && walletForm.amount.$error.required" translate>{{'amount_is_required'}}</span> <span class="help-block" ng-show="walletForm.amount.$dirty && walletForm.amount.$error.required" translate>{{'amount_is_required'}}</span>
<span class="help-block" ng-show="walletForm.amount.$dirty && walletForm.amount.$error.min">{{ 'amount_minimum_1' | translate }} {{currencySymbol}}.</span> <span class="help-block" ng-show="walletForm.amount.$dirty && walletForm.amount.$error.min">{{ 'amount_minimum_1' | translate }} {{currencySymbol}}.</span>
</div> </div>
<div class="text-right amountGroup m-t m-r-md"> <div class="text-right amountGroup m-t m-r-md" ng-class="{'has-error': walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$invalid }">
<span class="beforeAmount" translate>{{ 'confirm_credit_label' }}</span> <label for="amount_confirm" class="beforeAmount" translate>{{ 'confirm_credit_label' }}</label>
<input class="form-control m-l" <input class="form-control m-l"
type="number" type="number"
id="amount_confirm"
name="amount_confirm" name="amount_confirm"
ng-model="amount_confirm" ng-model="amount_confirm"
required required
@ -30,12 +38,53 @@
<span class="help-block" ng-show="walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$error.required" translate>{{'amount_confirm_is_required'}}</span> <span class="help-block" ng-show="walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$error.required" translate>{{'amount_confirm_is_required'}}</span>
<span class="help-block" ng-show="walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$error.pattern">{{ 'amount_confirm_does_not_match' | translate }}</span> <span class="help-block" ng-show="walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$error.pattern">{{ 'amount_confirm_does_not_match' | translate }}</span>
</div> </div>
</form>
<div class="alert alert-warning m-t-md m-b-sm"> <hr/>
<i class="fa fa-warning m-sm inline" aria-hidden="true"></i> <div class="text-right m-t">
<div class="inline pull-right width-90 m-t-n-xs" translate>{{ 'warning_uneditable_credit' }}</div> <label for="generate_avoir" translate>{{ 'generate_a_refund_invoice' }}</label>
</div> <div class="inline m-l">
<input bs-switch
ng-model="generate_avoir"
id="generate_avoir"
name="generate_avoir"
type="checkbox"
switch-on-text="{{ 'yes' | translate }}"
switch-off-text="{{ 'no' | translate }}"
switch-animate="true"/>
</div>
</div>
<div ng-show="generate_avoir">
<div class="m-t" ng-class="{'has-error': walletForm.avoir_date.$dirty && walletForm.avoir_date.$invalid }">
<label for="avoir_date" translate>{{ 'creation_date_for_the_refund' }}</label>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text"
class="form-control"
id="avoir_date"
name="avoir_date"
ng-model="avoir_date"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
placeholder="{{datePicker.format}}"
ng-click="toggleDatePicker($event)"
ng-required="generate_avoir"/>
</div>
<span class="help-block" ng-show="walletForm.avoir_date.$dirty && walletForm.avoir_date.$error.required" translate>{{ 'creation_date_is_required' }}</span>
</div>
<div class="m-t">
<label for="description" translate>{{ 'description_(optional)' }}</label>
<p translate>{{ 'will_appear_on_the_refund_invoice' }}</p>
<textarea class="form-control m-t-sm"
id="description"
name="description"
ng-model="description">
</textarea>
</div>
</div>
</form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -90,6 +90,6 @@ class API::CouponsController < API::ApiController
end end
def coupon_editable_params def coupon_editable_params
params.require(:coupon).permit(:name, :active) params.require(:coupon).permit(:name, :active, :valid_until)
end end
end end

View File

@ -57,7 +57,7 @@ class API::InvoicesController < API::ApiController
end end
# only for create avoir # only for create refund invoices (avoir)
def create def create
authorize Invoice authorize Invoice
invoice = Invoice.only_invoice.find(avoir_params[:invoice_id]) invoice = Invoice.only_invoice.find(avoir_params[:invoice_id])

View File

@ -14,13 +14,22 @@ class API::WalletController < API::ApiController
end end
def credit def credit
@wallet = Wallet.find(params[:id]) @wallet = Wallet.find(credit_params[:id])
authorize @wallet authorize @wallet
service = WalletService.new(user: current_user, wallet: @wallet) service = WalletService.new(user: current_user, wallet: @wallet)
if service.credit(params[:amount].to_f) transaction = service.credit(credit_params[:amount].to_f)
if transaction
if credit_params[:avoir]
service.create_avoir(transaction, credit_params[:avoir_date], credit_params[:avoir_description])
end
render :show render :show
else else
head 422 head 422
end end
end end
private
def credit_params
params.permit(:id, :amount, :avoir, :avoir_date, :avoir_description)
end
end end

View File

@ -11,6 +11,7 @@ class Coupon < ActiveRecord::Base
validates :validity_per_user, presence: true validates :validity_per_user, presence: true
validates :validity_per_user, inclusion: { in: %w(once forever) } validates :validity_per_user, inclusion: { in: %w(once forever) }
validates_with CouponDiscountValidator validates_with CouponDiscountValidator
validates_with CouponExpirationValidator
def safe_destroy def safe_destroy
if self.invoices.size == 0 if self.invoices.size == 0
@ -76,6 +77,7 @@ class Coupon < ActiveRecord::Base
attached_object: self attached_object: self
end end
private
def create_stripe_coupon def create_stripe_coupon
StripeWorker.perform_async(:create_stripe_coupon, id) StripeWorker.perform_async(:create_stripe_coupon, id)
end end

View File

@ -37,10 +37,12 @@ module PDF
if Setting.find_by({name: 'invoice_code-active'}).value == 'true' if Setting.find_by({name: 'invoice_code-active'}).value == 'true'
text I18n.t('invoices.code', CODE:Setting.find_by({name: 'invoice_code-value'}).value), :leading => 3 text I18n.t('invoices.code', CODE:Setting.find_by({name: 'invoice_code-value'}).value), :leading => 3
end end
if invoice.is_a?(Avoir) if invoice.invoiced_type != WalletTransaction.name
text I18n.t('invoices.order_number', NUMBER:invoice.invoice.order_number), :leading => 3 if invoice.is_a?(Avoir)
else text I18n.t('invoices.order_number', NUMBER:invoice.invoice.order_number), :leading => 3
text I18n.t('invoices.order_number', NUMBER:invoice.order_number), :leading => 3 else
text I18n.t('invoices.order_number', NUMBER:invoice.order_number), :leading => 3
end
end end
if invoice.is_a?(Avoir) if invoice.is_a?(Avoir)
text I18n.t('invoices.refund_invoice_issued_on_DATE', DATE:I18n.l(invoice.avoir_date.to_date)) text I18n.t('invoices.refund_invoice_issued_on_DATE', DATE:I18n.l(invoice.avoir_date.to_date))
@ -68,7 +70,11 @@ module PDF
# object # object
move_down 25 move_down 25
if invoice.is_a?(Avoir) if invoice.is_a?(Avoir)
object = I18n.t('invoices.cancellation_of_invoice_REF', REF: invoice.invoice.reference) if invoice.invoiced_type == WalletTransaction.name
object = I18n.t('invoices.wallet_credit')
else
object = I18n.t('invoices.cancellation_of_invoice_REF', REF: invoice.invoice.reference)
end
else else
case invoice.invoiced_type case invoice.invoiced_type
when 'Reservation' when 'Reservation'
@ -115,7 +121,7 @@ module PDF
else ### Reservation else ### Reservation
case invoice.invoiced.reservable_type case invoice.invoiced.try(:reservable_type)
### Machine reservation ### Machine reservation
when 'Machine' when 'Machine'
details += I18n.t('invoices.machine_reservation_DESCRIPTION', DESCRIPTION: item.description) details += I18n.t('invoices.machine_reservation_DESCRIPTION', DESCRIPTION: item.description)
@ -130,6 +136,9 @@ module PDF
invoice.invoiced.tickets.each do |t| invoice.invoiced.tickets.each do |t|
details += "\n "+I18n.t('invoices.other_rate_ticket', count: t.booked, NAME: t.event_price_category.price_category.name) details += "\n "+I18n.t('invoices.other_rate_ticket', count: t.booked, NAME: t.event_price_category.price_category.name)
end end
### wallet credit
when nil
details = item.description
### Other cases (not expected) ### Other cases (not expected)
else else

View File

@ -21,7 +21,7 @@ class WalletService
end end
raise ActiveRecord::Rollback raise ActiveRecord::Rollback
end end
return false false
end end
## debit an amount to wallet, if debit success then return a wallet transaction ## debit an amount to wallet, if debit success then return a wallet transaction
@ -35,6 +35,27 @@ class WalletService
end end
raise ActiveRecord::Rollback raise ActiveRecord::Rollback
end end
return false false
end
## create a refund invoice associated with the given wallet transaction
def create_avoir(wallet_transaction, avoir_date, description)
avoir = Avoir.new
avoir.type = 'Avoir'
avoir.invoiced = wallet_transaction
avoir.avoir_date = avoir_date
avoir.created_at = avoir_date
avoir.description = description
avoir.avoir_mode = 'wallet'
avoir.subscription_to_expire = false
avoir.user_id = wallet_transaction.wallet.user_id
avoir.total = wallet_transaction.amount * 100.0
avoir.save!
ii = InvoiceItem.new
ii.amount = wallet_transaction.amount * 100.0
ii.description = I18n.t('invoices.wallet_credit')
ii.invoice = avoir
ii.save!
end end
end end

View File

@ -1,15 +1,15 @@
class CouponDiscountValidator < ActiveModel::Validator class CouponDiscountValidator < ActiveModel::Validator
def validate(record) def validate(record)
if !record.percent_off.nil? if !record.percent_off.nil?
unless [0..100].include? record.percent_off unless (0..100).include? record.percent_off
record.errors[:percent_off] << 'Percentage must be included between 0 and 100' record.errors[:percent_off] << I18n.t('errors.messages.percentage_out_of_range')
end end
elsif !record.amount_off.nil? elsif !record.amount_off.nil?
unless record.amount_off > 0 unless record.amount_off > 0
record.errors[:amount_off] << I18n.t('errors.messages.greater_than_or_equal_to', count: 0) record.errors[:amount_off] << I18n.t('errors.messages.greater_than_or_equal_to', count: 0)
end end
else else
record.errors[:percent_off] << 'cannot be blank when amount_off is blank too' record.errors[:percent_off] << I18n.t('errors.messages.cannot_be_blank_at_same_time', field: 'amount_off')
end end
end end
end end

View File

@ -0,0 +1,19 @@
class CouponExpirationValidator < ActiveModel::Validator
##
# @param record {Coupon}
##
def validate(record)
previous = record.valid_until_was
current = record.valid_until
unless current.blank?
if current.end_of_day < Time.now
record.errors[:valid_until] << I18n.t('errors.messages.cannot_be_in_the_past')
end
if !previous.blank? and current.end_of_day < previous.end_of_day
record.errors[:valid_until] << I18n.t('errors.messages.cannot_be_before_previous_value')
end
end
end
end

View File

@ -1,3 +1,8 @@
client = Elasticsearch::Client.new host: "http://#{Rails.application.secrets.elaticsearch_host}:9200", log: true if Rails.env.test?
client = Elasticsearch::Client.new host: "http://#{Rails.application.secrets.elaticsearch_host}:9200", log: false
else
client = Elasticsearch::Client.new host: "http://#{Rails.application.secrets.elaticsearch_host}:9200", log: true
end
Elasticsearch::Model.client = client Elasticsearch::Model.client = client
Elasticsearch::Persistence.client = client Elasticsearch::Persistence.client = client

View File

@ -336,6 +336,7 @@ en:
groups: "Groups" groups: "Groups"
authentication: "Authentication" authentication: "Authentication"
do_you_really_want_to_delete_this_administrator_this_cannot_be_undone: "Do you really want to delete this administrator? This cannot be undone." do_you_really_want_to_delete_this_administrator_this_cannot_be_undone: "Do you really want to delete this administrator? This cannot be undone."
this_may_take_a_while_please_wait: "Warning: this may take a while, please be patient."
administrator_successfully_deleted: "Administrator successfully deleted." administrator_successfully_deleted: "Administrator successfully deleted."
unable_to_delete_the_administrator: "Unable to delete the administrator." unable_to_delete_the_administrator: "Unable to delete the administrator."
add_a_group: "Add a group" add_a_group: "Add a group"

View File

@ -336,6 +336,7 @@ fr:
groups: "Groupes" groups: "Groupes"
authentication: "Authentification" authentication: "Authentification"
do_you_really_want_to_delete_this_administrator_this_cannot_be_undone: "Êtes-vous sûr de vouloir supprimer cet administrateur ? Cette opération est irréversible." do_you_really_want_to_delete_this_administrator_this_cannot_be_undone: "Êtes-vous sûr de vouloir supprimer cet administrateur ? Cette opération est irréversible."
this_may_take_a_while_please_wait: "Attention : ceci peut prendre un certain temps, merci de patienter."
administrator_successfully_deleted: "L'administrateur a bien été supprimé." administrator_successfully_deleted: "L'administrateur a bien été supprimé."
unable_to_delete_the_administrator: "L'administrateur n'a pas pu être supprimé." unable_to_delete_the_administrator: "L'administrateur n'a pas pu être supprimé."
add_a_group: "Ajouter un groupe" add_a_group: "Ajouter un groupe"

View File

@ -324,6 +324,11 @@ en:
credit_title: 'Credit wallet' credit_title: 'Credit wallet'
credit_label: 'Set the amount to be credited' credit_label: 'Set the amount to be credited'
confirm_credit_label: 'Confirm 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' to_credit: 'Credit'
wallet_credit_successfully: "Wallet of user is credited successfully." wallet_credit_successfully: "Wallet of user is credited successfully."
a_problem_occurred_for_wallet_credit: "A problem is occurred while taking the credit of wallet" a_problem_occurred_for_wallet_credit: "A problem is occurred while taking the credit of wallet"

View File

@ -324,6 +324,11 @@ fr:
credit_title: 'Créditer le porte-monnaie' credit_title: 'Créditer le porte-monnaie'
credit_label: 'Indiquez le montant à créditer' credit_label: 'Indiquez le montant à créditer'
confirm_credit_label: 'Confirmez 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' to_credit: 'Créditer'
wallet_credit_successfully: "Le porte-monnaie de l'utilisateur a été chargé avec succès." wallet_credit_successfully: "Le porte-monnaie de l'utilisateur a été chargé avec succès."
a_problem_occurred_for_wallet_credit: "Un problème est survenu lors du chargement du porte-monnaie." a_problem_occurred_for_wallet_credit: "Un problème est survenu lors du chargement du porte-monnaie."

View File

@ -32,6 +32,10 @@ en:
size_too_small: "is too small (should be at least %{file_size})" size_too_small: "is too small (should be at least %{file_size})"
size_too_big: "is too big (should be at most %{file_size})" size_too_big: "is too big (should be at most %{file_size})"
export_not_found: "Requested export was not found. It was probably deleted, please generate a new export." export_not_found: "Requested export was not found. It was probably deleted, please generate a new export."
percentage_out_of_range: "Percentage must be included between 0 and 100"
cannot_be_blank_at_same_time: "cannot be blank when %{field} is blank too"
cannot_be_in_the_past: "cannot be in the past"
cannot_be_before_previous_value: "cannot be before the previous value"
activemodel: activemodel:
errors: errors:
@ -68,6 +72,7 @@ en:
order_number: "Order #: %{NUMBER}" order_number: "Order #: %{NUMBER}"
invoice_issued_on_DATE: "Invoice issued on %{DATE}" invoice_issued_on_DATE: "Invoice issued on %{DATE}"
refund_invoice_issued_on_DATE: "Refund invoice issued on %{DATE}" refund_invoice_issued_on_DATE: "Refund invoice issued on %{DATE}"
wallet_credit: "Wallet credit"
cancellation_of_invoice_REF: "Cancellation of invoice %{REF}" cancellation_of_invoice_REF: "Cancellation of invoice %{REF}"
reservation_of_USER_on_DATE_at_TIME: "Reservation of %{USER} on %{DATE} at %{TIME}" reservation_of_USER_on_DATE_at_TIME: "Reservation of %{USER} on %{DATE} at %{TIME}"
cancellation: "Cancellation" cancellation: "Cancellation"

View File

@ -32,6 +32,10 @@ fr:
size_too_small: "est trop petite (au moins %{file_size})" size_too_small: "est trop petite (au moins %{file_size})"
size_too_big: "est trop grande (pas plus de %{file_size})" size_too_big: "est trop grande (pas plus de %{file_size})"
export_not_found: "L'export demandé n'a pas été trouvé. Il a probablement été supprimé, veuillez lancer la génération d'un nouvel export." export_not_found: "L'export demandé n'a pas été trouvé. Il a probablement été supprimé, veuillez lancer la génération d'un nouvel export."
percentage_out_of_range: "Le pourcentage doit être inclus entre 0 et 100"
cannot_be_blank_at_same_time: "ou %{field} doit être rempli(e)"
cannot_be_in_the_past: "ne peut pas être dans le passé"
cannot_be_before_previous_value: "ne peut pas être antérieur(e) à la valeur précédente"
activemodel: activemodel:
errors: errors:
@ -68,6 +72,7 @@ fr:
order_number: "N° Commande : %{NUMBER}" order_number: "N° Commande : %{NUMBER}"
invoice_issued_on_DATE: "Facture éditée le %{DATE}" invoice_issued_on_DATE: "Facture éditée le %{DATE}"
refund_invoice_issued_on_DATE: "Avoir édité le %{DATE}" refund_invoice_issued_on_DATE: "Avoir édité le %{DATE}"
wallet_credit: "Crédit du porte-monnaie"
cancellation_of_invoice_REF: "Annulation de la facture %{REF}" cancellation_of_invoice_REF: "Annulation de la facture %{REF}"
reservation_of_USER_on_DATE_at_TIME: "Réservation de %{USER} le %{DATE} à %{TIME}" reservation_of_USER_on_DATE_at_TIME: "Réservation de %{USER} le %{DATE} à %{TIME}"
cancellation: "Annulation" cancellation: "Annulation"

View File

@ -1,42 +0,0 @@
class InsertCustomAggregations < ActiveRecord::Migration
def up
# available reservations hours for machines
machine = StatisticIndex.find_by(es_type_key: 'machine')
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: machine.id)
available_hours = StatisticCustomAggregation.new({
statistic_type_id: machine_hours.id,
es_index: 'fablab',
es_type: 'availabilities',
field: 'available_hours',
query: '{"size":0, "aggregations":{"%{aggs_name}":{"sum":{"field":"hours_duration"}}}, "query":{"bool":{"must":[{"range":{"start_at":{"gte":"%{start_date}", "lte":"%{end_date}"}}}, {"match":{"available_type":"machines"}}]}}}'
})
available_hours.save!
# available training tickets
training = StatisticIndex.find_by(es_type_key: 'training')
training_bookings = StatisticType.find_by(key: 'booking', statistic_index_id: training.id)
available_tickets = StatisticCustomAggregation.new({
statistic_type_id: training_bookings.id,
es_index: 'fablab',
es_type: 'availabilities',
field: 'available_tickets',
query: '{"size":0, "aggregations":{"%{aggs_name}":{"sum":{"field":"nb_total_places"}}}, "query":{"bool":{"must":[{"range":{"start_at":{"gte":"%{start_date}", "lte":"%{end_date}"}}}, {"match":{"available_type":"training"}}]}}}'
})
available_tickets.save!
end
def down
machine = StatisticIndex.find_by(es_type_key: 'machine')
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: machine.id)
StatisticCustomAggregation.where(field: 'available_hours', statistic_type_id: machine_hours.id).first.destroy!
training = StatisticIndex.find_by(es_type_key: 'training')
training_bookings = StatisticType.find_by(key: 'booking', statistic_index_id: training.id)
StatisticCustomAggregation.where(field: 'available_tickets', statistic_type_id: training_bookings.id).first.destroy!
end
end

View File

@ -396,4 +396,30 @@ unless Setting.find_by(name: 'reminder_delay').try(:value)
setting = Setting.find_or_initialize_by(name: 'reminder_delay') setting = Setting.find_or_initialize_by(name: 'reminder_delay')
setting.value = '24' setting.value = '24'
setting.save setting.save
end
if StatisticCustomAggregation.count == 0
# available reservations hours for machines
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2)
available_hours = StatisticCustomAggregation.new({
statistic_type_id: machine_hours.id,
es_index: 'fablab',
es_type: 'availabilities',
field: 'available_hours',
query: '{"size":0, "aggregations":{"%{aggs_name}":{"sum":{"field":"hours_duration"}}}, "query":{"bool":{"must":[{"range":{"start_at":{"gte":"%{start_date}", "lte":"%{end_date}"}}}, {"match":{"available_type":"machines"}}]}}}'
})
available_hours.save!
# available training tickets
training_bookings = StatisticType.find_by(key: 'booking', statistic_index_id: 3)
available_tickets = StatisticCustomAggregation.new({
statistic_type_id: training_bookings.id,
es_index: 'fablab',
es_type: 'availabilities',
field: 'available_tickets',
query: '{"size":0, "aggregations":{"%{aggs_name}":{"sum":{"field":"nb_total_places"}}}, "query":{"bool":{"must":[{"range":{"start_at":{"gte":"%{start_date}", "lte":"%{end_date}"}}}, {"match":{"available_type":"training"}}]}}}'
})
available_tickets.save!
end end

View File

@ -125,7 +125,7 @@ Requires=docker.service
[Service] [Service]
Type=oneshot Type=oneshot
ExecStart=/usr/bin/docker run --rm --name letsencrypt -v "/home/core/fabmanager/log:/var/log/letsencrypt" -v "/home/core/fabmanager/letsencrypt/etc:/etc/letsencrypt" -v "/home/core/fabmanager/letsencrypt/config:/letsencrypt-config" quay.io/letsencrypt/letsencrypt:latest -c "/letsencrypt-config/webroot.ini" certonly ExecStart=/usr/bin/docker run --rm --name letsencrypt -v "/home/core/fabmanager/log:/var/log/letsencrypt" -v "/home/core/fabmanager/letsencrypt/etc:/etc/letsencrypt" -v "/home/core/fabmanager/letsencrypt/config:/letsencrypt-config" quay.io/letsencrypt/letsencrypt:latest -c "/letsencrypt-config/webroot.ini" certonly
ExecStartPost=-/usr/bin/docker restart fabmanager ExecStartPost=-/usr/bin/docker restart fabmanager_nginx_1
``` ```
Create file (with sudo) /etc/systemd/system/letsencrypt.timer with Create file (with sudo) /etc/systemd/system/letsencrypt.timer with
@ -138,6 +138,9 @@ Requires=docker.service
OnCalendar=*-*-1 06:00:00 OnCalendar=*-*-1 06:00:00
Persistent=true Persistent=true
Unit=letsencrypt.service Unit=letsencrypt.service
[Install]
WantedBy=timers.target
``` ```
Then deploy your app and read the "Generate SSL certificate by Letsencrypt" section to complete the installation of the letsencrypt certificate. Then deploy your app and read the "Generate SSL certificate by Letsencrypt" section to complete the installation of the letsencrypt certificate.
@ -277,7 +280,9 @@ Remove your app and Run your app to apply changes
Finally, if everything is ok, start letsencrypt timer to update the certificate every 1st of the month : Finally, if everything is ok, start letsencrypt timer to update the certificate every 1st of the month :
```bash ```bash
sudo systemctl enable letsencrypt.timer
sudo systemctl start letsencrypt.timer sudo systemctl start letsencrypt.timer
(check) sudo systemctl list-timers
``` ```

View File

@ -1,7 +1,7 @@
module Events module Events
class AsUserTest < ActionDispatch::IntegrationTest class AsUserTest < ActionDispatch::IntegrationTest
test 'reserve event with many prices and payment means' do test 'reserve event with many prices and payment means and VAT' do
vlonchamp = User.find_by(username: 'vlonchamp') vlonchamp = User.find_by(username: 'vlonchamp')
login_as(vlonchamp, scope: :user) login_as(vlonchamp, scope: :user)
@ -15,6 +15,15 @@ module Events
users_credit_count = UsersCredit.count users_credit_count = UsersCredit.count
wallet_transactions_count = WalletTransaction.count wallet_transactions_count = WalletTransaction.count
# Enable the VAT at 19.6%
vat_active = Setting.find_by(name: 'invoice_VAT-active')
vat_active.value = 'true'
vat_active.save!
vat_rate = Setting.find_by(name: 'invoice_VAT-rate')
vat_rate.value = '19.6'
vat_rate.save!
# Reserve the 'radio' event # Reserve the 'radio' event
VCR.use_cassette('reserve_event_with_many_prices_and_payment_means') do VCR.use_cassette('reserve_event_with_many_prices_and_payment_means') do
post reservations_path, { post reservations_path, {

View File

@ -0,0 +1,82 @@
module Events
class AsAdminTest < ActionDispatch::IntegrationTest
setup do
admin = User.with_role(:admin).first
login_as(admin, scope: :user)
end
test 'compute price for a simple training' do
user = User.find_by(username: 'jdupond')
availability = Availability.find(2)
printer_training = availability.trainings.first
post '/api/prices/compute',
{
reservation: {
user_id: user.id,
reservable_id: printer_training.id,
reservable_type: printer_training.class.name,
slots_attributes: [
{
availability_id: availability.id,
end_at: availability.end_at,
offered: false,
start_at: availability.start_at
}
]
}
}.to_json,
default_headers
# Check response format & status
assert_equal 200, response.status, response.body
assert_equal Mime::JSON, response.content_type
# Check the price was computed correctly
price = json_response(response.body)
assert_equal (printer_training.trainings_pricings.where(group_id: user.group_id).first.amount / 100.0), price[:price], 'Computed price did not match training price'
end
test 'compute price for a machine reservation with an offered slot and a subscription' do
user = User.find_by(username: 'jdupond')
availability = Availability.find(3)
laser = availability.machines.where(id: 1).first
plan = Plan.where(group_id: user.group_id, interval: 'month').first
post '/api/prices/compute',
{
reservation: {
user_id: user.id,
reservable_id: laser.id,
reservable_type: laser.class.name,
plan_id: plan.id,
slots_attributes: [
{
availability_id: availability.id,
end_at: (availability.start_at + 1.hour).strftime('%Y-%m-%d %H:%M:%S.%9N Z'),
offered: true,
start_at: availability.start_at.strftime('%Y-%m-%d %H:%M:%S.%9N Z')
},
{
availability_id: availability.id,
end_at: (availability.start_at + 2.hour).strftime('%Y-%m-%d %H:%M:%S.%9N Z'),
offered: false,
start_at: (availability.start_at + 1.hour).strftime('%Y-%m-%d %H:%M:%S.%9N Z')
}
]
}
}.to_json,
default_headers
# Check response format & status
assert_equal 200, response.status, response.body
assert_equal Mime::JSON, response.content_type
# Check the event was created correctly
price = json_response(response.body)
assert_equal ((laser.prices.where(group_id: user.group_id, plan_id: plan.id).first.amount + plan.amount) / 100.0), price[:price], 'Computed price did not match machine + subscription price'
end
end
end

View File

@ -74,5 +74,38 @@ class WalletsTest < ActionDispatch::IntegrationTest
w.reload w.reload
assert_equal w.amount, expected_amount assert_equal w.amount, expected_amount
assert_equal w.amount, wallet[:amount] assert_equal w.amount, wallet[:amount]
# no refund invoices should have been generated
assert_empty Invoice.where(invoiced: w.wallet_transactions.last)
end
test 'admin credit wallet with refund invoice generation' do
admin = users(:user_1)
login_as(admin, scope: :user)
w = @vlonchamp.wallet
amount = 10
avoir_date = Time.now.end_of_day
expected_amount = w.amount + amount
put "/api/wallet/#{w.id}/credit",
{
amount: amount,
avoir: true,
avoir_date: avoir_date,
avoir_description: 'Some text'
}
assert_equal 200, response.status
assert_equal Mime::JSON, response.content_type
wallet = json_response(response.body)
w.reload
assert_equal w.amount, expected_amount
assert_equal w.amount, wallet[:amount]
# refund invoice must be generated
invoice = Invoice.where(invoiced: w.wallet_transactions.last).first
assert_equal amount, (invoice.total / 100.0), 'Avoir total does not match the amount credited to the wallet'
assert_equal amount, (invoice.invoice_items.first.amount / 100.0), 'Invoice item amount does not match'
assert_invoice_pdf invoice
end end
end end

View File

@ -1,6 +1,14 @@
require 'test_helper' require 'test_helper'
class CouponTest < ActiveSupport::TestCase class CouponTest < ActiveSupport::TestCase
test 'valid coupon with percentage' do
c = Coupon.new({name: 'Hot deals', code: 'HOT15', percent_off: 15, validity_per_user: 'once', valid_until: (Time.now + 2.weeks), max_usages: 100, active: true})
assert c.valid?
assert_equal 'active', c.status, 'Invalid coupon status'
assert_equal 'percent_off', c.type, 'Invalid coupon type'
end
test 'coupon must have a valid percentage' do test 'coupon must have a valid percentage' do
c = Coupon.new({name: 'Amazing deal', code: 'DISCOUNT', percent_off: 200, validity_per_user: 'once'}) c = Coupon.new({name: 'Amazing deal', code: 'DISCOUNT', percent_off: 200, validity_per_user: 'once'})
assert c.invalid? assert c.invalid?
@ -16,9 +24,11 @@ class CouponTest < ActiveSupport::TestCase
assert c.invalid? assert c.invalid?
end end
test 'coupon with cash amount has amount_off type' do test 'valid coupon with cash amount' do
c = Coupon.new({name: 'Essential Box', code: 'KWXX2M', amount_off: 2000, validity_per_user: 'once', max_usages: 1}) c = Coupon.new({name: 'Essential Box', code: 'KWXX2M', amount_off: 2000, validity_per_user: 'once', max_usages: 1, active: true})
assert_equal 'amount_off', c.type assert c.valid?
assert_equal 'active', c.status, 'Invalid coupon status'
assert_equal 'amount_off', c.type, 'Invalid coupon type'
end end
test 'coupon with cash amount cannot be used with cheaper cart' do test 'coupon with cash amount cannot be used with cheaper cart' do

View File

@ -1,3 +1,6 @@
require 'coveralls'
Coveralls.wear!('rails')
ENV['RAILS_ENV'] ||= 'test' ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__) require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help' require 'rails/test_help'
@ -68,6 +71,33 @@ class ActiveSupport::TestCase
assert File.exist?(invoice.file), 'Invoice PDF was not generated' assert File.exist?(invoice.file), 'Invoice PDF was not generated'
# now we check the file content
reader = PDF::Reader.new(invoice.file)
assert_equal 1, reader.page_count # single page invoice
ht_amount = invoice.total
page = reader.pages.first
lines = page.text.scan(/^.+/)
lines.each do |line|
# check that the numbers printed into the PDF file match the total stored in DB
if line.include? I18n.t('invoices.total_amount')
assert_equal invoice.total / 100.0, parse_amount_from_invoice_line(line), 'Invoice total rendered in the PDF file does not match'
end
# check that the VAT was correctly applied if it was configured
if line.include? I18n.t('invoices.including_total_excluding_taxes')
ht_amount = parse_amount_from_invoice_line(line)
end
end
if Setting.find_by(name: 'invoice_VAT-active').value == 'true'
vat_rate = Setting.find_by({name: 'invoice_VAT-rate'}).value.to_f
computed_ht = sprintf('%.2f', (invoice.total / (vat_rate / 100 + 1)) / 100.0).to_f
assert_equal computed_ht, ht_amount, 'Total excluding taxes rendered in the PDF file is not computed correctly'
else
assert_equal invoice.total, ht_amount, 'VAT information was rendered in the PDF file despite that VAT was disabled'
end
File.delete(invoice.file) File.delete(invoice.file)
end end
@ -89,6 +119,15 @@ class ActiveSupport::TestCase
skip('Unable to test export which is not of the category "statistics"') skip('Unable to test export which is not of the category "statistics"')
end end
end end
private
# Parse a line of text read from a PDF file and return the price included inside
# Line of text should be of form 'Label $10.00'
# @returns {float}
def parse_amount_from_invoice_line line
line[line.rindex(' ')+1..-1].tr(I18n.t('number.currency.format.unit'), '').to_f
end
end end
class ActionDispatch::IntegrationTest class ActionDispatch::IntegrationTest