mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-29 18:52:22 +01:00
Merge branch 'dev' for release 2.4.7
This commit is contained in:
commit
96259ab257
@ -1 +1 @@
|
||||
2.4.6
|
||||
2.4.7
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -15,6 +15,7 @@
|
||||
# Ignore all logfiles and tempfiles.
|
||||
/log/*.log
|
||||
/tmp
|
||||
/coverage
|
||||
|
||||
/public/uploads
|
||||
/public/assets
|
||||
@ -39,5 +40,8 @@
|
||||
.vagrant
|
||||
.docker
|
||||
|
||||
# Do not versionate coveralls token
|
||||
.coveralls.yml
|
||||
|
||||
# Plugins are versioned is their own repository
|
||||
/plugins/*
|
||||
|
37
CHANGELOG.md
37
CHANGELOG.md
@ -1,9 +1,18 @@
|
||||
# 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
|
||||
- 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
|
||||
|
||||
@ -25,8 +34,8 @@
|
||||
## v2.4.3 2016 November 21
|
||||
|
||||
- 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: number of reserved seats for an event is always of 1 in the excel export of reservations
|
||||
- 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: 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: some graphs do not display: events, users, trainings and machine hours
|
||||
@ -44,18 +53,18 @@
|
||||
|
||||
## 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 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
|
||||
|
||||
## v2.4.0 2016 October 4
|
||||
|
||||
|
||||
- RSS feeds to follow new projects and events published
|
||||
- Use slugs in projects URL opened from notifications
|
||||
- Ask for confirmation on machine deletion from the public view
|
||||
- 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
|
||||
- Share project with Facebook/Twitter
|
||||
- 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
|
||||
- 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
|
||||
- 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: Event categories, themes and age ranges can be customized
|
||||
- Filter events by category, theme and age range in public view
|
||||
- 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"
|
||||
- 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
|
||||
- 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
|
||||
@ -93,16 +102,16 @@
|
||||
- More file types allowed as project CAD attachements
|
||||
- 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 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
|
||||
- SSO: documentation improved with an usage example
|
||||
- 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: 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: 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: move event reservation is not limited by admin settings (prior-delay & disable)
|
||||
- Fix a bug: UI issues on small devices (dashboard + admin views)
|
||||
@ -134,7 +143,7 @@
|
||||
- [TODO DEPLOY] `bundle install` and `rake db:migrate`
|
||||
|
||||
## 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
|
||||
- Admin: Category is mandatory when creating an event
|
||||
|
||||
@ -150,7 +159,7 @@
|
||||
- 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: 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
|
||||
- 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
|
||||
|
3
Gemfile
3
Gemfile
@ -52,6 +52,8 @@ group :development do
|
||||
gem 'capistrano-maintenance', '0.0.5', require: false
|
||||
|
||||
gem 'active_record_query_trace'
|
||||
|
||||
gem 'coveralls', require: false
|
||||
end
|
||||
|
||||
group :test do
|
||||
@ -62,6 +64,7 @@ group :test do
|
||||
gem 'webmock'
|
||||
gem 'vcr'
|
||||
gem 'byebug'
|
||||
gem 'pdf-reader'
|
||||
end
|
||||
|
||||
group :production do
|
||||
|
29
Gemfile.lock
29
Gemfile.lock
@ -1,6 +1,7 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
Ascii85 (1.0.2)
|
||||
aasm (4.1.0)
|
||||
actionmailer (4.2.5)
|
||||
actionpack (= 4.2.5)
|
||||
@ -41,6 +42,7 @@ GEM
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.3.8)
|
||||
afm (0.2.2)
|
||||
ansi (1.5.0)
|
||||
api-pagination (4.3.0)
|
||||
apipie-rails (0.3.6)
|
||||
@ -118,6 +120,12 @@ GEM
|
||||
sass-rails (<= 5.0.1)
|
||||
sprockets (< 2.13)
|
||||
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)
|
||||
safe_yaml (~> 1.0.0)
|
||||
database_cleaner (1.4.1)
|
||||
@ -133,6 +141,7 @@ GEM
|
||||
warden (~> 1.2.3)
|
||||
devise-async (0.9.0)
|
||||
devise (~> 3.2)
|
||||
docile (1.1.5)
|
||||
domain_name (0.5.25)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
elasticsearch (1.0.12)
|
||||
@ -177,6 +186,7 @@ GEM
|
||||
has_secure_token (1.0.0)
|
||||
activerecord (>= 3.0)
|
||||
hashdiff (0.3.0)
|
||||
hashery (2.1.2)
|
||||
hashie (3.4.2)
|
||||
highline (1.7.1)
|
||||
hike (1.2.3)
|
||||
@ -265,6 +275,12 @@ GEM
|
||||
httparty (~> 0.13)
|
||||
orm_adapter (0.5.0)
|
||||
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)
|
||||
pkg-config (1.1.7)
|
||||
prawn (2.0.1)
|
||||
@ -335,6 +351,7 @@ GEM
|
||||
netrc (~> 0.7)
|
||||
rolify (4.0.0)
|
||||
ruby-progressbar (1.7.5)
|
||||
ruby-rc4 (0.1.5)
|
||||
rubyzip (1.1.7)
|
||||
rufus-scheduler (3.0.9)
|
||||
tzinfo
|
||||
@ -365,6 +382,11 @@ GEM
|
||||
sidekiq (>= 2.17.3)
|
||||
tilt (< 2.0.0)
|
||||
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)
|
||||
rack (~> 1.4)
|
||||
rack-protection (~> 1.4)
|
||||
@ -382,6 +404,8 @@ GEM
|
||||
stripe (1.30.2)
|
||||
json (~> 1.8.1)
|
||||
rest-client (~> 1.4)
|
||||
term-ansicolor (1.3.2)
|
||||
tins (~> 1.0)
|
||||
test_after_commit (1.0.0)
|
||||
activerecord (>= 3.2)
|
||||
therubyracer (0.12.0)
|
||||
@ -392,6 +416,7 @@ GEM
|
||||
tilt (1.4.1)
|
||||
timers (4.0.1)
|
||||
hitimes
|
||||
tins (1.13.0)
|
||||
ttfunk (1.4.0)
|
||||
twitter (5.14.0)
|
||||
addressable (~> 2.3)
|
||||
@ -462,6 +487,7 @@ DEPENDENCIES
|
||||
chroma
|
||||
coffee-rails (~> 4.1.0)
|
||||
compass-rails (= 2.0.4)
|
||||
coveralls
|
||||
database_cleaner
|
||||
devise
|
||||
devise-async
|
||||
@ -488,6 +514,7 @@ DEPENDENCIES
|
||||
omniauth
|
||||
omniauth-oauth2
|
||||
openlab_ruby
|
||||
pdf-reader
|
||||
pg
|
||||
prawn
|
||||
prawn-table
|
||||
@ -522,4 +549,4 @@ DEPENDENCIES
|
||||
webmock
|
||||
|
||||
BUNDLED WITH
|
||||
1.12.5
|
||||
1.13.1
|
||||
|
@ -58,8 +58,8 @@ Application.Controllers.controller "NewCouponController", ["$scope", "$state", '
|
||||
##
|
||||
# Controller used in the coupon edition page
|
||||
##
|
||||
Application.Controllers.controller "EditCouponController", ["$scope", "$state", 'Coupon', 'couponPromise', '_t'
|
||||
, ($scope, $state, Coupon, couponPromise, _t) ->
|
||||
Application.Controllers.controller "EditCouponController", ["$scope", "$state", 'Coupon', 'couponPromise', '_t', 'growl'
|
||||
, ($scope, $state, Coupon, couponPromise, _t, growl) ->
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
|
||||
@ -73,6 +73,9 @@ Application.Controllers.controller "EditCouponController", ["$scope", "$state",
|
||||
## Options for the validity per user
|
||||
$scope.validities = userValidities
|
||||
|
||||
## Mapping for validation errors
|
||||
$scope.errors = {}
|
||||
|
||||
## Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection)
|
||||
$scope.datePicker =
|
||||
format: Fablab.uibDateFormat
|
||||
@ -98,11 +101,12 @@ Application.Controllers.controller "EditCouponController", ["$scope", "$state",
|
||||
# Callback to save the coupon's changes to the API
|
||||
##
|
||||
$scope.updateCoupon = ->
|
||||
$scope.errors = {}
|
||||
Coupon.update {id: $scope.coupon.id}, coupon: $scope.coupon, (coupon) ->
|
||||
$state.go('app.admin.pricing')
|
||||
, (err)->
|
||||
growl.error(_t('unable_to_update_the_coupon_an_error_occurred'))
|
||||
console.error(err)
|
||||
$scope.errors = err.data
|
||||
|
||||
|
||||
|
||||
|
@ -105,8 +105,8 @@ class MembersController
|
||||
##
|
||||
# Controller used in the members/groups management page
|
||||
##
|
||||
Application.Controllers.controller "AdminMembersController", ["$scope", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export'
|
||||
, ($scope, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export) ->
|
||||
Application.Controllers.controller "AdminMembersController", ["$scope","$sce", '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:
|
||||
object: ->
|
||||
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
|
||||
Admin.delete id: admin.id, ->
|
||||
admins.splice(findAdminIdxById(admins, admin.id), 1)
|
||||
@ -423,11 +423,40 @@ Application.Controllers.controller "EditMemberController", ["$scope", "$state",
|
||||
templateUrl: '<%= asset_path "wallet/credit_modal.html" %>'
|
||||
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
|
||||
##
|
||||
$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'))
|
||||
$uibModalInstance.close(_wallet)
|
||||
|
@ -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>
|
||||
</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>
|
||||
<div class="input-group">
|
||||
<input type="text" id="coupon[valid_until]"
|
||||
@ -92,16 +92,16 @@
|
||||
datepicker-options="datePicker.options"
|
||||
is-open="datePicker.opened"
|
||||
min-date="datePicker.minDate"
|
||||
ng-disabled="mode == 'EDIT'"
|
||||
ng-click="toggleDatePicker($event)"/>
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
<span class="help-block error" ng-show="errors['valid_until']">{{ errors['valid_until'].join(' ; ') }}</span>
|
||||
|
||||
<span class="help-block text-info text-xs">
|
||||
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
|
||||
</span>
|
||||
<span class="text-info text-xs">
|
||||
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{'has-error': couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$invalid}">
|
||||
@ -114,7 +114,7 @@
|
||||
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 text-info text-xs">
|
||||
<span class="text-info text-xs">
|
||||
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<h1>{{object.title}}</h1>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{object.msg}}</p>
|
||||
<p ng-bind-html="object.msg"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-info" ng-click="ok()" translate>{{ 'confirm' }}</button>
|
||||
|
@ -3,11 +3,18 @@
|
||||
<h1 translate>{{ 'credit_title' }}</h1>
|
||||
</div>
|
||||
<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}">
|
||||
<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"
|
||||
type="number"
|
||||
id="amount"
|
||||
name="amount"
|
||||
ng-model="amount"
|
||||
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.min">{{ 'amount_minimum_1' | translate }} {{currencySymbol}}.</span>
|
||||
</div>
|
||||
<div class="text-right amountGroup m-t m-r-md">
|
||||
<span class="beforeAmount" translate>{{ 'confirm_credit_label' }}</span>
|
||||
<div class="text-right amountGroup m-t m-r-md" ng-class="{'has-error': walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$invalid }">
|
||||
<label for="amount_confirm" class="beforeAmount" translate>{{ 'confirm_credit_label' }}</label>
|
||||
<input class="form-control m-l"
|
||||
type="number"
|
||||
id="amount_confirm"
|
||||
name="amount_confirm"
|
||||
ng-model="amount_confirm"
|
||||
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.pattern">{{ 'amount_confirm_does_not_match' | translate }}</span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="alert alert-warning m-t-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>
|
||||
<hr/>
|
||||
<div class="text-right m-t">
|
||||
<label for="generate_avoir" translate>{{ 'generate_a_refund_invoice' }}</label>
|
||||
<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 class="modal-footer">
|
||||
|
@ -90,6 +90,6 @@ class API::CouponsController < API::ApiController
|
||||
end
|
||||
|
||||
def coupon_editable_params
|
||||
params.require(:coupon).permit(:name, :active)
|
||||
params.require(:coupon).permit(:name, :active, :valid_until)
|
||||
end
|
||||
end
|
||||
|
@ -57,7 +57,7 @@ class API::InvoicesController < API::ApiController
|
||||
|
||||
end
|
||||
|
||||
# only for create avoir
|
||||
# only for create refund invoices (avoir)
|
||||
def create
|
||||
authorize Invoice
|
||||
invoice = Invoice.only_invoice.find(avoir_params[:invoice_id])
|
||||
|
@ -14,13 +14,22 @@ class API::WalletController < API::ApiController
|
||||
end
|
||||
|
||||
def credit
|
||||
@wallet = Wallet.find(params[:id])
|
||||
@wallet = Wallet.find(credit_params[:id])
|
||||
authorize @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
|
||||
else
|
||||
head 422
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def credit_params
|
||||
params.permit(:id, :amount, :avoir, :avoir_date, :avoir_description)
|
||||
end
|
||||
end
|
||||
|
@ -11,6 +11,7 @@ class Coupon < ActiveRecord::Base
|
||||
validates :validity_per_user, presence: true
|
||||
validates :validity_per_user, inclusion: { in: %w(once forever) }
|
||||
validates_with CouponDiscountValidator
|
||||
validates_with CouponExpirationValidator
|
||||
|
||||
def safe_destroy
|
||||
if self.invoices.size == 0
|
||||
@ -76,6 +77,7 @@ class Coupon < ActiveRecord::Base
|
||||
attached_object: self
|
||||
end
|
||||
|
||||
private
|
||||
def create_stripe_coupon
|
||||
StripeWorker.perform_async(:create_stripe_coupon, id)
|
||||
end
|
||||
|
@ -37,10 +37,12 @@ module PDF
|
||||
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
|
||||
end
|
||||
if invoice.is_a?(Avoir)
|
||||
text I18n.t('invoices.order_number', NUMBER:invoice.invoice.order_number), :leading => 3
|
||||
else
|
||||
text I18n.t('invoices.order_number', NUMBER:invoice.order_number), :leading => 3
|
||||
if invoice.invoiced_type != WalletTransaction.name
|
||||
if invoice.is_a?(Avoir)
|
||||
text I18n.t('invoices.order_number', NUMBER:invoice.invoice.order_number), :leading => 3
|
||||
else
|
||||
text I18n.t('invoices.order_number', NUMBER:invoice.order_number), :leading => 3
|
||||
end
|
||||
end
|
||||
if invoice.is_a?(Avoir)
|
||||
text I18n.t('invoices.refund_invoice_issued_on_DATE', DATE:I18n.l(invoice.avoir_date.to_date))
|
||||
@ -68,7 +70,11 @@ module PDF
|
||||
# object
|
||||
move_down 25
|
||||
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
|
||||
case invoice.invoiced_type
|
||||
when 'Reservation'
|
||||
@ -115,7 +121,7 @@ module PDF
|
||||
|
||||
|
||||
else ### Reservation
|
||||
case invoice.invoiced.reservable_type
|
||||
case invoice.invoiced.try(:reservable_type)
|
||||
### Machine reservation
|
||||
when 'Machine'
|
||||
details += I18n.t('invoices.machine_reservation_DESCRIPTION', DESCRIPTION: item.description)
|
||||
@ -130,6 +136,9 @@ module PDF
|
||||
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)
|
||||
end
|
||||
### wallet credit
|
||||
when nil
|
||||
details = item.description
|
||||
|
||||
### Other cases (not expected)
|
||||
else
|
||||
|
@ -21,7 +21,7 @@ class WalletService
|
||||
end
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
return false
|
||||
false
|
||||
end
|
||||
|
||||
## debit an amount to wallet, if debit success then return a wallet transaction
|
||||
@ -35,6 +35,27 @@ class WalletService
|
||||
end
|
||||
raise ActiveRecord::Rollback
|
||||
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
|
||||
|
@ -1,15 +1,15 @@
|
||||
class CouponDiscountValidator < ActiveModel::Validator
|
||||
def validate(record)
|
||||
if !record.percent_off.nil?
|
||||
unless [0..100].include? record.percent_off
|
||||
record.errors[:percent_off] << 'Percentage must be included between 0 and 100'
|
||||
unless (0..100).include? record.percent_off
|
||||
record.errors[:percent_off] << I18n.t('errors.messages.percentage_out_of_range')
|
||||
end
|
||||
elsif !record.amount_off.nil?
|
||||
unless record.amount_off > 0
|
||||
record.errors[:amount_off] << I18n.t('errors.messages.greater_than_or_equal_to', count: 0)
|
||||
end
|
||||
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
|
19
app/validators/coupon_expiration_validator.rb
Normal file
19
app/validators/coupon_expiration_validator.rb
Normal 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
|
@ -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::Persistence.client = client
|
||||
|
@ -336,6 +336,7 @@ en:
|
||||
groups: "Groups"
|
||||
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."
|
||||
this_may_take_a_while_please_wait: "Warning: this may take a while, please be patient."
|
||||
administrator_successfully_deleted: "Administrator successfully deleted."
|
||||
unable_to_delete_the_administrator: "Unable to delete the administrator."
|
||||
add_a_group: "Add a group"
|
||||
|
@ -336,6 +336,7 @@ fr:
|
||||
groups: "Groupes"
|
||||
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."
|
||||
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é."
|
||||
unable_to_delete_the_administrator: "L'administrateur n'a pas pu être supprimé."
|
||||
add_a_group: "Ajouter un groupe"
|
||||
|
@ -324,6 +324,11 @@ en:
|
||||
credit_title: 'Credit wallet'
|
||||
credit_label: 'Set the amount to be credited'
|
||||
confirm_credit_label: 'Confirm the amount to be credited'
|
||||
generate_a_refund_invoice: "Generate a refund invoice"
|
||||
creation_date_for_the_refund: "Creation date for the refund"
|
||||
creation_date_is_required: "Creation date is required."
|
||||
description_(optional): "Description (optional):"
|
||||
will_appear_on_the_refund_invoice: "Will appear on the refund invoice."
|
||||
to_credit: 'Credit'
|
||||
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"
|
||||
|
@ -324,6 +324,11 @@ fr:
|
||||
credit_title: 'Créditer le porte-monnaie'
|
||||
credit_label: 'Indiquez le montant à créditer'
|
||||
confirm_credit_label: 'Confirmez le montant à créditer'
|
||||
generate_a_refund_invoice: "Générer une facture d'avoir"
|
||||
creation_date_for_the_refund: "Date d'émission de l'avoir"
|
||||
creation_date_is_required: "La date d'émission est requise."
|
||||
description_(optional): "Description (optionnelle) :"
|
||||
will_appear_on_the_refund_invoice: "Apparaîtra sur la facture de remboursement."
|
||||
to_credit: 'Créditer'
|
||||
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."
|
||||
|
@ -32,6 +32,10 @@ en:
|
||||
size_too_small: "is too small (should be at least %{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."
|
||||
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:
|
||||
errors:
|
||||
@ -68,6 +72,7 @@ en:
|
||||
order_number: "Order #: %{NUMBER}"
|
||||
invoice_issued_on_DATE: "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}"
|
||||
reservation_of_USER_on_DATE_at_TIME: "Reservation of %{USER} on %{DATE} at %{TIME}"
|
||||
cancellation: "Cancellation"
|
||||
|
@ -32,6 +32,10 @@ fr:
|
||||
size_too_small: "est trop petite (au moins %{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."
|
||||
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:
|
||||
errors:
|
||||
@ -68,6 +72,7 @@ fr:
|
||||
order_number: "N° Commande : %{NUMBER}"
|
||||
invoice_issued_on_DATE: "Facture éditée 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}"
|
||||
reservation_of_USER_on_DATE_at_TIME: "Réservation de %{USER} le %{DATE} à %{TIME}"
|
||||
cancellation: "Annulation"
|
||||
|
@ -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
|
26
db/seeds.rb
26
db/seeds.rb
@ -396,4 +396,30 @@ unless Setting.find_by(name: 'reminder_delay').try(:value)
|
||||
setting = Setting.find_or_initialize_by(name: 'reminder_delay')
|
||||
setting.value = '24'
|
||||
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
|
@ -125,7 +125,7 @@ Requires=docker.service
|
||||
[Service]
|
||||
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
|
||||
ExecStartPost=-/usr/bin/docker restart fabmanager
|
||||
ExecStartPost=-/usr/bin/docker restart fabmanager_nginx_1
|
||||
```
|
||||
|
||||
Create file (with sudo) /etc/systemd/system/letsencrypt.timer with
|
||||
@ -138,6 +138,9 @@ Requires=docker.service
|
||||
OnCalendar=*-*-1 06:00:00
|
||||
Persistent=true
|
||||
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.
|
||||
@ -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 :
|
||||
|
||||
```bash
|
||||
sudo systemctl enable letsencrypt.timer
|
||||
sudo systemctl start letsencrypt.timer
|
||||
(check) sudo systemctl list-timers
|
||||
```
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
module Events
|
||||
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')
|
||||
login_as(vlonchamp, scope: :user)
|
||||
@ -15,6 +15,15 @@ module Events
|
||||
users_credit_count = UsersCredit.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
|
||||
VCR.use_cassette('reserve_event_with_many_prices_and_payment_means') do
|
||||
post reservations_path, {
|
||||
|
82
test/integration/prices/compute_test.rb
Normal file
82
test/integration/prices/compute_test.rb
Normal 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
|
@ -74,5 +74,38 @@ class WalletsTest < ActionDispatch::IntegrationTest
|
||||
w.reload
|
||||
assert_equal w.amount, expected_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
|
||||
|
@ -1,6 +1,14 @@
|
||||
require 'test_helper'
|
||||
|
||||
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
|
||||
c = Coupon.new({name: 'Amazing deal', code: 'DISCOUNT', percent_off: 200, validity_per_user: 'once'})
|
||||
assert c.invalid?
|
||||
@ -16,9 +24,11 @@ class CouponTest < ActiveSupport::TestCase
|
||||
assert c.invalid?
|
||||
end
|
||||
|
||||
test 'coupon with cash amount has amount_off type' do
|
||||
c = Coupon.new({name: 'Essential Box', code: 'KWXX2M', amount_off: 2000, validity_per_user: 'once', max_usages: 1})
|
||||
assert_equal 'amount_off', c.type
|
||||
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, active: true})
|
||||
assert c.valid?
|
||||
assert_equal 'active', c.status, 'Invalid coupon status'
|
||||
assert_equal 'amount_off', c.type, 'Invalid coupon type'
|
||||
end
|
||||
|
||||
test 'coupon with cash amount cannot be used with cheaper cart' do
|
||||
|
@ -1,3 +1,6 @@
|
||||
require 'coveralls'
|
||||
Coveralls.wear!('rails')
|
||||
|
||||
ENV['RAILS_ENV'] ||= 'test'
|
||||
require File.expand_path('../../config/environment', __FILE__)
|
||||
require 'rails/test_help'
|
||||
@ -68,6 +71,33 @@ class ActiveSupport::TestCase
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
@ -89,6 +119,15 @@ class ActiveSupport::TestCase
|
||||
skip('Unable to test export which is not of the category "statistics"')
|
||||
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
|
||||
|
||||
class ActionDispatch::IntegrationTest
|
||||
|
Loading…
x
Reference in New Issue
Block a user