1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-17 06:52:27 +01:00

Merge branch 'wallet_avoir' into dev

This commit is contained in:
Sylvain 2016-12-13 15:33:56 +01:00
commit 76c1412177
12 changed files with 239 additions and 20 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,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"

View File

@ -72,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"

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

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