1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-29 10:24:20 +01:00

Merge branch 'monthly-payment' into staging

This commit is contained in:
Sylvain 2020-12-21 16:34:20 +01:00
commit 3541688c03
35 changed files with 1930 additions and 1528 deletions

View File

@ -16,6 +16,7 @@ Metrics/BlockLength:
- 'lib/tasks/**/*.rake' - 'lib/tasks/**/*.rake'
- 'config/routes.rb' - 'config/routes.rb'
- 'app/pdfs/pdf/*.rb' - 'app/pdfs/pdf/*.rb'
- 'test/**/*.rb'
Metrics/ParameterLists: Metrics/ParameterLists:
CountKeywordArgs: false CountKeywordArgs: false
Style/BracesAroundHashParameters: Style/BracesAroundHashParameters:

View File

@ -1,6 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake, # Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require File.expand_path('../config/application', __FILE__) require_relative 'config/application'
Rails.application.load_tasks Rails.application.load_tasks

View File

@ -49,7 +49,7 @@ class API::PaymentsController < API::ApiController
if params[:cart_items][:reservation] if params[:cart_items][:reservation]
res = on_reservation_success(intent, amount[:details]) res = on_reservation_success(intent, amount[:details])
elsif params[:cart_items][:subscription] elsif params[:cart_items][:subscription]
res = on_subscription_success(intent) res = on_subscription_success(intent, amount[:details])
end end
end end
@ -72,7 +72,7 @@ class API::PaymentsController < API::ApiController
user = User.find(params[:user_id]) user = User.find(params[:user_id])
key = Setting.get('stripe_secret_key') key = Setting.get('stripe_secret_key')
@intent = Stripe::SetupIntent.create({ customer: user.stp_customer_id }, { api_key: key }) @intent = Stripe::SetupIntent.create({ customer: user.stp_customer_id }, { api_key: key })
render json: { client_secret: @intent.client_secret } render json: { id: @intent.id, client_secret: @intent.client_secret }
end end
def confirm_payment_schedule def confirm_payment_schedule
@ -84,7 +84,7 @@ class API::PaymentsController < API::ApiController
if params[:cart_items][:reservation] if params[:cart_items][:reservation]
res = on_reservation_success(intent, amount[:details]) res = on_reservation_success(intent, amount[:details])
elsif params[:cart_items][:subscription] elsif params[:cart_items][:subscription]
res = on_subscription_success(intent) res = on_subscription_success(intent, amount[:details])
end end
end end
@ -103,7 +103,11 @@ class API::PaymentsController < API::ApiController
current_user.id current_user.id
end end
is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id) is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id)
.pay_and_save(@reservation, payment_details: details, payment_intent_id: intent.id) .pay_and_save(@reservation,
payment_details: details,
payment_intent_id: intent.id,
schedule: params[:cart_items][:reservation][:payment_schedule],
payment_method: params[:cart_items][:reservation][:payment_method])
if intent.class == Stripe::PaymentIntent if intent.class == Stripe::PaymentIntent
Stripe::PaymentIntent.update( Stripe::PaymentIntent.update(
intent.id, intent.id,
@ -121,7 +125,7 @@ class API::PaymentsController < API::ApiController
end end
end end
def on_subscription_success(intent) def on_subscription_success(intent, details)
@subscription = Subscription.new(subscription_params) @subscription = Subscription.new(subscription_params)
user_id = if current_user.admin? || current_user.manager? user_id = if current_user.admin? || current_user.manager?
params[:cart_items][:subscription][:user_id] params[:cart_items][:subscription][:user_id]
@ -130,8 +134,7 @@ class API::PaymentsController < API::ApiController
end end
is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id) is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id)
.pay_and_save(@subscription, .pay_and_save(@subscription,
coupon: coupon_params[:coupon_code], payment_details: details,
invoice: true,
payment_intent_id: intent.id, payment_intent_id: intent.id,
schedule: params[:cart_items][:subscription][:payment_schedule], schedule: params[:cart_items][:subscription][:payment_schedule],
payment_method: 'stripe') payment_method: 'stripe')

View File

@ -35,7 +35,10 @@ class API::ReservationsController < API::ApiController
@reservation = Reservation.new(reservation_params) @reservation = Reservation.new(reservation_params)
is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id) is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id)
.pay_and_save(@reservation, payment_details: price[:price_details]) .pay_and_save(@reservation,
payment_details: price[:price_details],
schedule: params[:reservation][:payment_schedule],
payment_method: params[:reservation][:payment_method])
if is_reserve if is_reserve
SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible

View File

@ -14,14 +14,13 @@ class API::SubscriptionsController < API::ApiController
# Managers can create subscriptions for other users # Managers can create subscriptions for other users
def create def create
user_id = current_user.admin? || current_user.manager? ? params[:subscription][:user_id] : current_user.id user_id = current_user.admin? || current_user.manager? ? params[:subscription][:user_id] : current_user.id
amount = transaction_amount(current_user.admin? || (current_user.manager? && current_user.id != user_id), user_id) transaction = transaction_amount(current_user.admin? || (current_user.manager? && current_user.id != user_id), user_id)
authorize SubscriptionContext.new(Subscription, amount, user_id) authorize SubscriptionContext.new(Subscription, transaction[:amount], user_id)
@subscription = Subscription.new(subscription_params) @subscription = Subscription.new(subscription_params)
is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id) is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id)
.pay_and_save(@subscription, coupon: coupon_params[:coupon_code], .pay_and_save(@subscription, payment_details: transaction[:details],
invoice: true,
schedule: params[:subscription][:payment_schedule], schedule: params[:subscription][:payment_schedule],
payment_method: params[:subscription][:payment_method]) payment_method: params[:subscription][:payment_method])
@ -65,7 +64,7 @@ class API::SubscriptionsController < API::ApiController
# Subtract wallet amount from total # Subtract wallet amount from total
total = price_details[:total] total = price_details[:total]
wallet_debit = get_wallet_debit(user, total) wallet_debit = get_wallet_debit(user, total)
total - wallet_debit { amount: total - wallet_debit, details: price_details }
end end
def get_wallet_debit(user, total_amount) def get_wallet_debit(user, total_amount)

View File

@ -19,6 +19,13 @@ client.interceptors.response.use(function (response) {
}); });
function extractHumanReadableMessage(error: any): string { function extractHumanReadableMessage(error: any): string {
if (error.match(/^<!DOCTYPE html>/)) {
// parse ruby error pages
const parser = new DOMParser();
const htmlDoc = parser.parseFromString(error, 'text/html');
return htmlDoc.querySelector('h2').textContent;
}
if (typeof error === 'string') return error; if (typeof error === 'string') return error;
let message = ''; let message = '';

View File

@ -37,7 +37,7 @@
*/ */
class MembersController { class MembersController {
constructor ($scope, $state, Group, Training) { constructor ($scope, $state, Group, Training) {
// Retrieve the profiles groups (eg. students ...) // Retrieve the profiles groups (e.g. students ...)
Group.query(function (groups) { $scope.groups = groups.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; }); }); Group.query(function (groups) { $scope.groups = groups.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; }); });
// Retrieve the list of available trainings // Retrieve the list of available trainings
@ -62,7 +62,7 @@ class MembersController {
}; };
/** /**
* Shows the birth day datepicker * Shows the birthday datepicker
* @param $event {Object} jQuery event object * @param $event {Object} jQuery event object
*/ */
$scope.openDatePicker = function ($event) { $scope.openDatePicker = function ($event) {
@ -85,7 +85,7 @@ class MembersController {
* For use with ngUpload (https://github.com/twilson63/ngUpload). * For use with ngUpload (https://github.com/twilson63/ngUpload).
* Intended to be the callback when an upload is done: any raised error will be stacked in the * Intended to be the callback when an upload is done: any raised error will be stacked in the
* $scope.alerts array. If everything goes fine, the user is redirected to the members listing page. * $scope.alerts array. If everything goes fine, the user is redirected to the members listing page.
* @param content {Object} JSON - The upload's result * @param content {Object} JSON - The result of the upload
*/ */
$scope.submited = function (content) { $scope.submited = function (content) {
if ((content.id == null)) { if ((content.id == null)) {
@ -110,7 +110,7 @@ class MembersController {
/** /**
* For use with 'ng-class', returns the CSS class name for the uploads previews. * For use with 'ng-class', returns the CSS class name for the uploads previews.
* The preview may show a placeholder or the content of the file depending on the upload state. * The preview may show a placeholder, or the content of the file depending on the upload state.
* @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules) * @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
*/ */
$scope.fileinputClass = function (v) { $scope.fileinputClass = function (v) {
@ -143,7 +143,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
searchText: '', searchText: '',
// Members ordering/sorting. Default: not sorted // Members ordering/sorting. Default: not sorted
order: 'id', order: 'id',
// currently displayed page of members // the currently displayed page of members
page: 1, page: 1,
// true when all members where loaded // true when all members where loaded
noMore: false, noMore: false,
@ -158,7 +158,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
}; };
// admins list // admins list
$scope.admins = adminsPromise.admins.filter(function(m) { return m.id != Fablab.superadminId; }); $scope.admins = adminsPromise.admins.filter(function (m) { return m.id !== Fablab.superadminId; });
// Admins ordering/sorting. Default: not sorted // Admins ordering/sorting. Default: not sorted
$scope.orderAdmin = null; $scope.orderAdmin = null;
@ -210,7 +210,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
* @param orderPartner {string} ordering criterion * @param orderPartner {string} ordering criterion
*/ */
$scope.setOrderPartner = function (orderPartner) { $scope.setOrderPartner = function (orderPartner) {
if ($scope.orderPartner === orderPartner) { if ($scope.orderPartner === orderPartner) {
return $scope.orderPartner = `-${orderPartner}`; return $scope.orderPartner = `-${orderPartner}`;
} else { } else {
return $scope.orderPartner = orderPartner; return $scope.orderPartner = orderPartner;
@ -229,7 +229,6 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
} }
}; };
/** /**
* Open a modal dialog allowing the admin to create a new partner user * Open a modal dialog allowing the admin to create a new partner user
*/ */
@ -265,12 +264,11 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
}); });
}; };
/** /**
* Ask for confirmation then delete the specified user * Ask for confirmation then delete the specified user
* @param memberId {number} identifier of the user to delete * @param memberId {number} identifier of the user to delete
*/ */
$scope.deleteMember = function(memberId) { $scope.deleteMember = function (memberId) {
dialogs.confirm( dialogs.confirm(
{ {
resolve: { resolve: {
@ -289,11 +287,14 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
$scope.members.splice(findItemIdxById($scope.members, memberId), 1); $scope.members.splice(findItemIdxById($scope.members, memberId), 1);
return growl.success(_t('app.admin.members.member_successfully_deleted')); return growl.success(_t('app.admin.members.member_successfully_deleted'));
}, },
function (error) { growl.error(_t('app.admin.members.unable_to_delete_the_member')); } function (error) {
growl.error(_t('app.admin.members.unable_to_delete_the_member'));
console.error(error);
}
); );
} }
); );
} };
/** /**
* Ask for confirmation then delete the specified administrator * Ask for confirmation then delete the specified administrator
@ -319,7 +320,10 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
admins.splice(findItemIdxById(admins, admin.id), 1); admins.splice(findItemIdxById(admins, admin.id), 1);
return growl.success(_t('app.admin.members.administrator_successfully_deleted')); return growl.success(_t('app.admin.members.administrator_successfully_deleted'));
}, },
function (error) { growl.error(_t('app.admin.members.unable_to_delete_the_administrator')); } function (error) {
growl.error(_t('app.admin.members.unable_to_delete_the_administrator'));
console.error(error);
}
); );
} }
); );
@ -349,11 +353,14 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
partners.splice(findItemIdxById(partners, partner.id), 1); partners.splice(findItemIdxById(partners, partner.id), 1);
return growl.success(_t('app.admin.members.partner_successfully_deleted')); return growl.success(_t('app.admin.members.partner_successfully_deleted'));
}, },
function (error) { growl.error(_t('app.admin.members.unable_to_delete_the_partner')); } function (error) {
growl.error(_t('app.admin.members.unable_to_delete_the_partner'));
console.error(error);
}
); );
} }
); );
} };
/** /**
* Ask for confirmation then delete the specified manager * Ask for confirmation then delete the specified manager
@ -379,11 +386,14 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
managers.splice(findItemIdxById(managers, manager.id), 1); managers.splice(findItemIdxById(managers, manager.id), 1);
return growl.success(_t('app.admin.members.manager_successfully_deleted')); return growl.success(_t('app.admin.members.manager_successfully_deleted'));
}, },
function (error) { growl.error(_t('app.admin.members.unable_to_delete_the_manager')); } function (error) {
growl.error(_t('app.admin.members.unable_to_delete_the_manager'));
console.error(error);
}
); );
} }
); );
} };
/** /**
* Callback for the 'load more' button. * Callback for the 'load more' button.
@ -399,7 +409,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
*/ */
$scope.updateTextSearch = function () { $scope.updateTextSearch = function () {
if (searchTimeout) clearTimeout(searchTimeout); if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(function() { searchTimeout = setTimeout(function () {
resetSearchMember(); resetSearchMember();
memberSearch(); memberSearch();
}, 300); }, 300);
@ -425,9 +435,8 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
}); });
}; };
/** /**
* Setup the feature-tour for the admin/members page. * Set up the feature-tour for the admin/members page.
* This is intended as a contextual help (when pressing F1) * This is intended as a contextual help (when pressing F1)
*/ */
$scope.setupMembersTour = function () { $scope.setupMembersTour = function () {
@ -570,7 +579,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('members') < 0) { if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('members') < 0) {
uitour.start(); uitour.start();
} }
} };
/* PRIVATE SCOPE */ /* PRIVATE SCOPE */
@ -586,22 +595,22 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
/** /**
* Will temporize the search query to prevent overloading the API * Will temporize the search query to prevent overloading the API
*/ */
var searchTimeout = null; let searchTimeout = null;
/** /**
* Iterate through the provided array and return the index of the requested item * Iterate through the provided array and return the index of the requested item
* @param items {Array} full list of users with role 'admin' * @param items {Array} full list of users with the 'admin' role
* @param id {Number} id of the item to retrieve in the list * @param id {Number} id of the item to retrieve in the list
* @returns {Number} index of the requested item, in the provided array * @returns {Number} index of the requested item, in the provided array
*/ */
var findItemIdxById = function (items, id) { const findItemIdxById = function (items, id) {
return (items.map(function (item) { return item.id; })).indexOf(id); return (items.map(function (item) { return item.id; })).indexOf(id);
}; };
/** /**
* Reinitialize the context of members's search to display new results set * Reinitialize the context of the search to display new results set
*/ */
var resetSearchMember = function () { const resetSearchMember = function () {
$scope.member.noMore = false; $scope.member.noMore = false;
$scope.member.page = 1; $scope.member.page = 1;
}; };
@ -609,9 +618,9 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
/** /**
* Run a search query with the current parameters set ($scope.member[searchText,order,page]) * Run a search query with the current parameters set ($scope.member[searchText,order,page])
* and affect or append the result in $scope.members, depending on the concat parameter * and affect or append the result in $scope.members, depending on the concat parameter
* @param [concat] {boolean} if true, the result will be append to $scope.members instead of being affected * @param [concat] {boolean} if true, the result will be appended to $scope.members instead of being replaced
*/ */
var memberSearch = function (concat) { const memberSearch = function (concat) {
Member.list({ Member.list({
query: { query: {
search: $scope.member.searchText, search: $scope.member.searchText,
@ -666,7 +675,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
// the user subscription // the user subscription
if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) { if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) {
$scope.subscription = $scope.user.subscription; $scope.subscription = $scope.user.subscription;
$scope.subscription.expired_at = $scope.subscription.expired_at;
} else { } else {
Plan.query({ group_id: $scope.user.group_id }, function (plans) { Plan.query({ group_id: $scope.user.group_id }, function (plans) {
$scope.plans = plans; $scope.plans = plans;
@ -696,16 +704,15 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
/** /**
* Open a modal dialog asking for confirmation to change the role of the given user * Open a modal dialog asking for confirmation to change the role of the given user
* @param userId {number} id of the user to "promote"
* @returns {*} * @returns {*}
*/ */
$scope.changeUserRole = function() { $scope.changeUserRole = function () {
const modalInstance = $uibModal.open({ const modalInstance = $uibModal.open({
animation: true, animation: true,
templateUrl: '/admin/members/change_role_modal.html', templateUrl: '/admin/members/change_role_modal.html',
size: 'lg', size: 'lg',
resolve: { resolve: {
user() { return $scope.user; } user () { return $scope.user; }
}, },
controller: ['$scope', '$uibModalInstance', 'Member', 'user', '_t', function ($scope, $uibModalInstance, Member, user, _t) { controller: ['$scope', '$uibModalInstance', 'Member', 'user', '_t', function ($scope, $uibModalInstance, Member, user, _t) {
$scope.user = user; $scope.user = user;
@ -715,7 +722,7 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
$scope.roles = [ $scope.roles = [
{ key: 'admin', label: _t('app.admin.members_edit.admin') }, { key: 'admin', label: _t('app.admin.members_edit.admin') },
{ key: 'manager', label: _t('app.admin.members_edit.manager'), notAnOption: (user.role === 'admin') }, { key: 'manager', label: _t('app.admin.members_edit.manager'), notAnOption: (user.role === 'admin') },
{ key: 'member', label: _t('app.admin.members_edit.member'), notAnOption: (user.role === 'admin' || user.role === 'manager') }, { key: 'member', label: _t('app.admin.members_edit.member'), notAnOption: (user.role === 'admin' || user.role === 'manager') }
]; ];
$scope.ok = function () { $scope.ok = function () {
@ -740,7 +747,7 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
return modalInstance.result.then(function (user) { return modalInstance.result.then(function (user) {
// remove the user for the old list add to the new // remove the user for the old list add to the new
}); });
} };
/** /**
* Open a modal dialog, allowing the admin to extend the current user's subscription (freely or not) * Open a modal dialog, allowing the admin to extend the current user's subscription (freely or not)
@ -778,7 +785,10 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
growl.success(_t('app.admin.members_edit.you_successfully_changed_the_expiration_date_of_the_user_s_subscription')); growl.success(_t('app.admin.members_edit.you_successfully_changed_the_expiration_date_of_the_user_s_subscription'));
return $uibModalInstance.close(_subscription); return $uibModalInstance.close(_subscription);
}, },
function (error) { growl.error(_t('app.admin.members_edit.a_problem_occurred_while_saving_the_date')); } function (error) {
growl.error(_t('app.admin.members_edit.a_problem_occurred_while_saving_the_date'));
console.error(error);
}
); );
}; };
@ -792,14 +802,14 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
/** /**
* Open a modal dialog allowing the admin to set a subscription for the given user. * Open a modal dialog allowing the admin to set a subscription for the given user.
* @param user {Object} User object, user currently reviewed, as recovered from GET /api/members/:id * @param user {Object} User object, user currently reviewed, as recovered from GET /api/members/:id
* @param plans {Array} List of plans, availables for the currently reviewed user, as recovered from GET /api/plans * @param plans {Array} List of plans, available for the currently reviewed user, as recovered from GET /api/plans
*/ */
$scope.createSubscriptionModal = function (user, plans) { $scope.createSubscriptionModal = function (user, plans) {
const modalInstance = $uibModal.open({ const modalInstance = $uibModal.open({
animation: true, animation: true,
templateUrl: '/admin/subscriptions/create_modal.html', templateUrl: '/admin/subscriptions/create_modal.html',
size: 'lg', size: 'lg',
controller: ['$scope', '$uibModalInstance', 'Subscription', 'Group', function ($scope, $uibModalInstance, Subscription, Group) { controller: ['$scope', '$uibModalInstance', 'Subscription', function ($scope, $uibModalInstance, Subscription) {
// selected user // selected user
$scope.user = user; $scope.user = user;
@ -810,7 +820,7 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
* Generate a string identifying the given plan by literal human-readable name * Generate a string identifying the given plan by literal human-readable name
* @param plan {Object} Plan object, as recovered from GET /api/plan/:id * @param plan {Object} Plan object, as recovered from GET /api/plan/:id
* @param groups {Array} List of Groups objects, as recovered from GET /api/groups * @param groups {Array} List of Groups objects, as recovered from GET /api/groups
* @param short {boolean} If true, the generated name will contains the group slug, otherwise the group full name * @param short {boolean} If true, the generated name will contain the group slug, otherwise the group full name
* will be included. * will be included.
* @returns {String} * @returns {String}
*/ */
@ -902,8 +912,9 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
*/ */
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
} }
] }); ]
// once the form was validated succesfully ... });
// once the form was validated successfully...
return modalInstance.result.then(function (wallet) { return modalInstance.result.then(function (wallet) {
$scope.wallet = wallet; $scope.wallet = wallet;
return Wallet.transactions({ id: wallet.id }, function (transactions) { $scope.transactions = transactions; }); return Wallet.transactions({ id: wallet.id }, function (transactions) { $scope.transactions = transactions; });
@ -923,13 +934,12 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
const initialize = function () { const initialize = function () {
CSRF.setMetaTags(); CSRF.setMetaTags();
// init the birth date to JS object // init the birthdate to JS object
$scope.user.statistic_profile.birthday = moment($scope.user.statistic_profile.birthday).toDate(); $scope.user.statistic_profile.birthday = moment($scope.user.statistic_profile.birthday).toDate();
// the user subscription // the user subscription
if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) { if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) {
$scope.subscription = $scope.user.subscription; $scope.subscription = $scope.user.subscription;
$scope.subscription.expired_at = $scope.subscription.expired_at;
} else { } else {
Plan.query({ group_id: $scope.user.group_id }, function (plans) { Plan.query({ group_id: $scope.user.group_id }, function (plans) {
$scope.plans = plans; $scope.plans = plans;
@ -996,7 +1006,7 @@ Application.Controllers.controller('NewMemberController', ['$scope', '$state', '
* Controller used in the member's import page: import from CSV (admin view) * Controller used in the member's import page: import from CSV (admin view)
*/ */
Application.Controllers.controller('ImportMembersController', ['$scope', '$state', 'Group', 'Training', 'CSRF', 'tags', 'growl', Application.Controllers.controller('ImportMembersController', ['$scope', '$state', 'Group', 'Training', 'CSRF', 'tags', 'growl',
function($scope, $state, Group, Training, CSRF, tags, growl) { function ($scope, $state, Group, Training, CSRF, tags, growl) {
CSRF.setMetaTags(); CSRF.setMetaTags();
/* PUBLIC SCOPE */ /* PUBLIC SCOPE */
@ -1008,19 +1018,19 @@ Application.Controllers.controller('ImportMembersController', ['$scope', '$state
$scope.method = 'post'; $scope.method = 'post';
// List of all tags // List of all tags
$scope.tags = tags $scope.tags = tags;
/* /*
* Callback run after the form was submitted * Callback run after the form was submitted
* @param content {*} The result provided by the server, may be an Import object or an error message * @param content {*} The result provided by the server, may be an Import object, or an error message
*/ */
$scope.onImportResult = function(content) { $scope.onImportResult = function (content) {
if (content.id) { if (content.id) {
$state.go('app.admin.members_import_result', { id: content.id }); $state.go('app.admin.members_import_result', { id: content.id });
} else { } else {
growl.error(JSON.stringify(content)); growl.error(JSON.stringify(content));
} }
} };
// Using the MembersController // Using the MembersController
return new MembersController($scope, $state, Group, Training); return new MembersController($scope, $state, Group, Training);
@ -1041,7 +1051,7 @@ Application.Controllers.controller('ImportMembersResultController', ['$scope', '
$scope.results = null; $scope.results = null;
/** /**
* Changes the admin's view to the members import page * Changes the view of the admin to the members import page
*/ */
$scope.cancel = function () { $state.go('app.admin.members_import'); }; $scope.cancel = function () { $state.go('app.admin.members_import'); };
@ -1053,8 +1063,8 @@ Application.Controllers.controller('ImportMembersResultController', ['$scope', '
const initialize = function () { const initialize = function () {
$scope.results = JSON.parse($scope.import.results); $scope.results = JSON.parse($scope.import.results);
if (!$scope.results) { if (!$scope.results) {
setTimeout(function() { setTimeout(function () {
Import.get({ id: $scope.import.id }, function(data) { Import.get({ id: $scope.import.id }, function (data) {
$scope.import = data; $scope.import = data;
initialize(); initialize();
}); });
@ -1068,69 +1078,68 @@ Application.Controllers.controller('ImportMembersResultController', ['$scope', '
]); ]);
/** /**
* Controller used in the admin's creation page (admin view) * Controller used in the admin creation page (admin view)
*/ */
Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'Admin', 'growl', '_t', 'phoneRequiredPromise', Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'Admin', 'growl', '_t', 'phoneRequiredPromise',
function ($state, $scope, Admin, growl, _t, phoneRequiredPromise) { function ($state, $scope, Admin, growl, _t, phoneRequiredPromise) {
// default admin profile // default admin profile
let getGender; let getGender;
$scope.admin = { $scope.admin = {
statistic_profile_attributes: { statistic_profile_attributes: {
gender: true gender: true
}, },
profile_attributes: {}, profile_attributes: {},
invoicing_profile_attributes: {} invoicing_profile_attributes: {}
}; };
// Default parameters for AngularUI-Bootstrap datepicker // Default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker = { $scope.datePicker = {
format: Fablab.uibDateFormat, format: Fablab.uibDateFormat,
opened: false, opened: false,
options: { options: {
startingDay: Fablab.weekStartingDay startingDay: Fablab.weekStartingDay
} }
}; };
// is the phone number required in _admin_form? // is the phone number required in _admin_form?
$scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true'); $scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true');
/** /**
* Shows the birth day datepicker * Shows the birthday datepicker
* @param $event {Object} jQuery event object
*/ */
$scope.openDatePicker = function ($event) { $scope.datePicker.opened = true; }; $scope.openDatePicker = function () { $scope.datePicker.opened = true; };
/** /**
* Send the new admin, currently stored in $scope.admin, to the server for database saving * Send the new admin, currently stored in $scope.admin, to the server for database saving
*/ */
$scope.saveAdmin = function () { $scope.saveAdmin = function () {
Admin.save( Admin.save(
{}, {},
{ admin: $scope.admin }, { admin: $scope.admin },
function () { function () {
growl.success(_t('app.admin.admins_new.administrator_successfully_created_he_will_receive_his_connection_directives_by_email', { GENDER: getGender($scope.admin) })); growl.success(_t('app.admin.admins_new.administrator_successfully_created_he_will_receive_his_connection_directives_by_email', { GENDER: getGender($scope.admin) }));
return $state.go('app.admin.members'); return $state.go('app.admin.members');
} }
, function (error) { , function (error) {
growl.error(_t('app.admin.admins_new.failed_to_create_admin') + JSON.stringify(error.data ? error.data : error)); growl.error(_t('app.admin.admins_new.failed_to_create_admin') + JSON.stringify(error.data ? error.data : error));
console.error(error); console.error(error);
} }
); );
}; };
/* PRIVATE SCOPE */ /* PRIVATE SCOPE */
/** /**
* Return an enumerable meaningful string for the gender of the provider user * Return an enumerable meaningful string for the gender of the provider user
* @param user {Object} Database user record * @param user {Object} Database user record
* @return {string} 'male' or 'female' * @return {string} 'male' or 'female'
*/ */
return getGender = function (user) { return getGender = function (user) {
if (user.statistic_profile_attributes) { if (user.statistic_profile_attributes) {
if (user.statistic_profile_attributes.gender) { return 'male'; } else { return 'female'; } if (user.statistic_profile_attributes.gender) { return 'male'; } else { return 'female'; }
} else { return 'other'; } } else { return 'other'; }
}; };
} }
]); ]);
@ -1140,65 +1149,64 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A
Application.Controllers.controller('NewManagerController', ['$state', '$scope', 'User', 'groupsPromise', 'tagsPromise', 'growl', '_t', Application.Controllers.controller('NewManagerController', ['$state', '$scope', 'User', 'groupsPromise', 'tagsPromise', 'growl', '_t',
function ($state, $scope, User, groupsPromise, tagsPromise, growl, _t) { function ($state, $scope, User, groupsPromise, tagsPromise, growl, _t) {
// default admin profile // default admin profile
$scope.manager = { $scope.manager = {
statistic_profile_attributes: { statistic_profile_attributes: {
gender: true gender: true
}, },
profile_attributes: {}, profile_attributes: {},
invoicing_profile_attributes: {} invoicing_profile_attributes: {}
}; };
// Default parameters for AngularUI-Bootstrap datepicker // Default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker = { $scope.datePicker = {
format: Fablab.uibDateFormat, format: Fablab.uibDateFormat,
opened: false, opened: false,
options: { options: {
startingDay: Fablab.weekStartingDay startingDay: Fablab.weekStartingDay
} }
}; };
// list of all groups // list of all groups
$scope.groups = groupsPromise.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; }); $scope.groups = groupsPromise.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; });
// list of all tags // list of all tags
$scope.tags = tagsPromise; $scope.tags = tagsPromise;
/** /**
* Shows the birth day datepicker * Shows the birthday datepicker
* @param $event {Object} jQuery event object
*/ */
$scope.openDatePicker = function ($event) { $scope.datePicker.opened = true; }; $scope.openDatePicker = function () { $scope.datePicker.opened = true; };
/** /**
* Send the new manager, currently stored in $scope.manager, to the server for database saving * Send the new manager, currently stored in $scope.manager, to the server for database saving
*/ */
$scope.saveManager = function () { $scope.saveManager = function () {
User.save( User.save(
{}, {},
{ manager: $scope.manager }, { manager: $scope.manager },
function () { function () {
growl.success(_t('app.admin.manager_new.manager_successfully_created', { GENDER: getGender($scope.manager) })); growl.success(_t('app.admin.manager_new.manager_successfully_created', { GENDER: getGender($scope.manager) }));
return $state.go('app.admin.members'); return $state.go('app.admin.members');
} }
, function (error) { , function (error) {
growl.error(_t('app.admin.admins_new.failed_to_create_manager') + JSON.stringify(error.data ? error.data : error)); growl.error(_t('app.admin.admins_new.failed_to_create_manager') + JSON.stringify(error.data ? error.data : error));
console.error(error); console.error(error);
} }
); );
}; };
/* PRIVATE SCOPE */ /* PRIVATE SCOPE */
/** /**
* Return an enumerable meaningful string for the gender of the provider user * Return an enumerable meaningful string for the gender of the provider user
* @param user {Object} Database user record * @param user {Object} Database user record
* @return {string} 'male' or 'female' * @return {string} 'male' or 'female'
*/ */
const getGender = function (user) { const getGender = function (user) {
if (user.statistic_profile_attributes) { if (user.statistic_profile_attributes) {
if (user.statistic_profile_attributes.gender) { return 'male'; } else { return 'female'; } if (user.statistic_profile_attributes.gender) { return 'male'; } else { return 'female'; }
} else { return 'other'; } } else { return 'other'; }
}; };
} }
]); ]);

View File

@ -716,9 +716,14 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
* Open a modal window that allows the user to process a credit card payment for his current shopping cart. * Open a modal window that allows the user to process a credit card payment for his current shopping cart.
*/ */
const payByStripe = function (reservation) { const payByStripe = function (reservation) {
$scope.toggleStripeModal(() => { // check that the online payment is enabled
$scope.stripe.cartItems = mkCartItems(reservation, 'stripe'); if ($scope.settings.online_payment_module !== 'true') {
}); growl.error(_t('app.shared.cart.online_payment_disabled'));
} else {
$scope.toggleStripeModal(() => {
$scope.stripe.cartItems = mkCartItems(reservation, 'stripe');
});
}
}; };
/** /**
* Open a modal window that allows the user to process a local payment for his current shopping cart (admin only). * Open a modal window that allows the user to process a local payment for his current shopping cart (admin only).
@ -751,10 +756,13 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
}, },
user () { user () {
return $scope.user; return $scope.user;
},
settings () {
return $scope.settings;
} }
}, },
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon', 'selectedPlan', 'schedule', 'cartItems', 'user', controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon', 'selectedPlan', 'schedule', 'cartItems', 'user', 'settings',
function ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, Subscription, wallet, helpers, $filter, coupon, selectedPlan, schedule, cartItems, user) { function ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, Subscription, wallet, helpers, $filter, coupon, selectedPlan, schedule, cartItems, user, settings) {
// user wallet amount // user wallet amount
$scope.wallet = wallet; $scope.wallet = wallet;
@ -797,7 +805,12 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
*/ */
$scope.ok = function () { $scope.ok = function () {
if ($scope.schedule && $scope.method.payment_method === 'stripe') { if ($scope.schedule && $scope.method.payment_method === 'stripe') {
return $scope.toggleStripeModal(); // check that the online payment is enabled
if (settings.online_payment_module !== 'true') {
return growl.error(_t('app.shared.cart.online_payment_disabled'));
} else {
return $scope.toggleStripeModal();
}
} }
$scope.attempting = true; $scope.attempting = true;
// save subscription (if there's only a subscription selected) // save subscription (if there's only a subscription selected)
@ -927,11 +940,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount); const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount);
if ((AuthService.isAuthorized(['member']) && amountToPay > 0) || if ((AuthService.isAuthorized(['member']) && amountToPay > 0) ||
(AuthService.isAuthorized('manager') && $scope.user.id === $rootScope.currentUser.id && amountToPay > 0)) { (AuthService.isAuthorized('manager') && $scope.user.id === $rootScope.currentUser.id && amountToPay > 0)) {
if ($scope.settings.online_payment_module !== 'true') { return payByStripe(reservation);
growl.error(_t('app.shared.cart.online_payment_disabled'));
} else {
return payByStripe(reservation);
}
} else { } else {
if (AuthService.isAuthorized(['admin']) || if (AuthService.isAuthorized(['admin']) ||
(AuthService.isAuthorized('manager') && $scope.user.id !== $rootScope.currentUser.id) || (AuthService.isAuthorized('manager') && $scope.user.id !== $rootScope.currentUser.id) ||

View File

@ -10,6 +10,7 @@
<div ng-hide="free"> <div ng-hide="free">
<p translate>{{ 'app.admin.members_edit.you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription' }}</p> <p translate>{{ 'app.admin.members_edit.you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription' }}</p>
<p translate>{{ 'app.admin.members_edit.credits_will_be_reset' }}</p> <p translate>{{ 'app.admin.members_edit.credits_will_be_reset' }}</p>
<p translate>{{ 'app.admin.members_edit.payment_scheduled' }}</p>
</div> </div>
</div> </div>
<form role="form" name="subscriptionForm" novalidate> <form role="form" name="subscriptionForm" novalidate>

View File

@ -33,6 +33,13 @@ class PaymentSchedule < ApplicationRecord
save save
end end
def set_wallet_transaction(amount, transaction_id)
raise InvalidFootprintError unless check_footprint
update_columns(wallet_amount: amount, wallet_transaction_id: transaction_id)
chain_record
end
def chain_record def chain_record
self.footprint = compute_footprint self.footprint = compute_footprint
save! save!
@ -46,4 +53,8 @@ class PaymentSchedule < ApplicationRecord
def compute_footprint def compute_footprint
FootprintService.compute_footprint(PaymentSchedule, self) FootprintService.compute_footprint(PaymentSchedule, self)
end end
def check_footprint
payment_schedule_items.map(&:check_footprint).all? && footprint == compute_footprint
end
end end

View File

@ -4,4 +4,23 @@
class PaymentScheduleItem < ApplicationRecord class PaymentScheduleItem < ApplicationRecord
belongs_to :payment_schedule belongs_to :payment_schedule
belongs_to :invoice belongs_to :invoice
after_create :chain_record
def chain_record
self.footprint = compute_footprint
save!
FootprintDebug.create!(
footprint: footprint,
data: FootprintService.footprint_data(PaymentScheduleItem, self),
klass: PaymentScheduleItem.name
)
end
def check_footprint
footprint == compute_footprint
end
def compute_footprint
FootprintService.compute_footprint(PaymentScheduleItem, self)
end
end end

View File

@ -30,133 +30,33 @@ class Reservation < ApplicationRecord
after_commit :notify_member_create_reservation, on: :create after_commit :notify_member_create_reservation, on: :create
after_commit :notify_admin_member_create_reservation, on: :create after_commit :notify_admin_member_create_reservation, on: :create
after_save :update_event_nb_free_places, if: proc { |reservation| reservation.reservable_type == 'Event' } after_save :update_event_nb_free_places, if: proc { |reservation| reservation.reservable_type == 'Event' }
after_create :debit_user_wallet
## ##
# Generate an array of {Stripe::InvoiceItem} with the elements in the current reservation, price included. # These checks will run before the invoice/payment-schedule is generated
# @param payment_details {Hash} as generated by Price.compute
## ##
def generate_invoice_items(payment_details = nil) def pre_check
# check that none of the reserved availabilities was locked # check that none of the reserved availabilities was locked
slots.each do |slot| slots.each do |slot|
raise LockedError if slot.availability.lock raise LockedError if slot.availability.lock
end end
case reservable
# === Event reservation ===
when Event
slots.each do |slot|
description = "#{reservable.name}\n"
description += if slot.start_at.to_date != slot.end_at.to_date
I18n.t('events.from_STARTDATE_to_ENDDATE',
STARTDATE: I18n.l(slot.start_at.to_date, format: :long),
ENDDATE: I18n.l(slot.end_at.to_date, format: :long)) + ' ' +
I18n.t('events.from_STARTTIME_to_ENDTIME',
STARTTIME: I18n.l(slot.start_at, format: :hour_minute),
ENDTIME: I18n.l(slot.end_at, format: :hour_minute))
else
"#{I18n.l slot.start_at.to_date, format: :long} #{I18n.l slot.start_at, format: :hour_minute}" \
" - #{I18n.l slot.end_at, format: :hour_minute}"
end
price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] }
invoice.invoice_items.push InvoiceItem.new(
amount: price_slot[:price],
description: description
)
end
# === Space|Machine|Training reservation ===
else
slots.each do |slot|
description = reservable.name +
" #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] }
invoice.invoice_items.push InvoiceItem.new(
amount: price_slot[:price],
description: description
)
end
end
# === Coupon ===
@coupon = payment_details[:coupon]
# === Wallet ===
@wallet_amount_debit = wallet_amount_debit
end end
# check reservation amount total and strip invoice total to pay is equal ## Generate the subscription associated with for the current reservation
# @param stp_invoice[Stripe::Invoice] def generate_subscription
# @param coupon_code[String] return unless plan_id
# return Boolean
def is_equal_reservation_total_and_stp_invoice_total(stp_invoice, coupon_code = nil) self.subscription = Subscription.find_or_initialize_by(statistic_profile_id: statistic_profile_id)
compute_amount_total_to_pay(coupon_code) == stp_invoice.total subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, expiration_date: nil }
subscription.init_save
subscription
end end
def clear_payment_info(card, invoice) ##
card&.delete # These actions will be realized after the reservation is initially saved (on creation)
if invoice ##
invoice.closed = true def post_save
invoice.save
end
rescue Stripe::InvalidRequestError => e
logger.error e
rescue Stripe::AuthenticationError => e
logger.error e
rescue Stripe::APIConnectionError => e
logger.error e
rescue Stripe::StripeError => e
logger.error e
rescue StandardError => e
logger.error e
end
def clean_pending_strip_invoice_items
pending_invoice_items = Stripe::InvoiceItem.list(
{ customer: user.stp_customer_id, limit: 100 },
{ api_key: Setting.get('stripe_secret_key') }
).data.select { |ii| ii.invoice.nil? }
pending_invoice_items.each(&:delete)
end
def save_with_payment(operator_profile_id, payment_details, payment_intent_id = nil)
operator = InvoicingProfile.find(operator_profile_id)&.user
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
build_invoice(
invoicing_profile: user.invoicing_profile,
statistic_profile: user.statistic_profile,
operator_profile_id: operator_profile_id,
stp_payment_intent_id: payment_intent_id,
payment_method: method
)
generate_invoice_items(payment_details)
return false unless valid?
if plan_id
self.subscription = Subscription.find_or_initialize_by(statistic_profile_id: statistic_profile_id)
subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, expiration_date: nil }
if subscription.save_with_payment(operator_profile_id, invoice: false)
invoice.invoice_items.push InvoiceItem.new(
amount: payment_details[:elements][:plan],
description: subscription.plan.name,
subscription_id: subscription.id
)
set_total_and_coupon(payment_details[:coupon])
save!
else
errors[:card] << subscription.errors[:card].join
return false
end
else
set_total_and_coupon(payment_details[:coupon])
save!
end
UsersCredits::Manager.new(reservation: self).update_credits UsersCredits::Manager.new(reservation: self).update_credits
true
end end
# @param canceled if true, count the number of seats for this reservation, including canceled seats # @param canceled if true, count the number of seats for this reservation, including canceled seats
@ -219,61 +119,4 @@ class Reservation < ApplicationRecord
receiver: User.admins_and_managers, receiver: User.admins_and_managers,
attached_object: self attached_object: self
end end
def cart_total
total = (invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) or 0)
if plan_id.present?
plan = Plan.find(plan_id)
total += plan.amount
end
total
end
def wallet_amount_debit
total = cart_total
total = CouponService.new.apply(total, @coupon, user.id) if @coupon
wallet_amount = (user.wallet.amount * 100).to_i
wallet_amount >= total ? total : wallet_amount
end
def debit_user_wallet
return unless @wallet_amount_debit.present? && @wallet_amount_debit != 0
amount = @wallet_amount_debit / 100.0
wallet_transaction = WalletService.new(user: user, wallet: user.wallet).debit(amount, self)
# wallet debit success
raise DebitWalletError unless wallet_transaction
invoice.set_wallet_transaction(@wallet_amount_debit, wallet_transaction.id)
end
# this function only use for compute total of reservation before save
def compute_amount_total_to_pay(coupon_code = nil)
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
unless coupon_code.nil?
cp = Coupon.find_by(code: coupon_code)
raise InvalidCouponError unless !cp.nil? && cp.status(user.id) == 'active'
total = CouponService.new.apply(total, cp, user.id)
end
total - wallet_amount_debit
end
##
# Set the total price to the reservation's invoice, summing its whole items.
# Additionally a coupon may be applied to this invoice to make a discount on the total price
# @param [coupon] {Coupon} optional coupon to apply to the invoice
##
def set_total_and_coupon(coupon = nil)
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
unless coupon.nil?
total = CouponService.new.apply(total, coupon, user.id)
invoice.coupon_id = coupon.id
end
invoice.total = total
end
end end

View File

@ -19,85 +19,20 @@ class Subscription < ApplicationRecord
after_save :notify_admin_subscribed_plan after_save :notify_admin_subscribed_plan
after_save :notify_partner_subscribed_plan, if: :of_partner_plan? after_save :notify_partner_subscribed_plan, if: :of_partner_plan?
# @param invoice if true then only the subscription is payed, without reservation ##
# if false then the subscription is payed with reservation # Set the inner properties of the subscription, init the user's credits and save the subscription into the DB
# @param payment_method is only used for schedules # @return {boolean} true, if the operation succeeded
def save_with_payment(operator_profile_id, invoice: true, coupon_code: nil, payment_intent_id: nil, schedule: nil, payment_method: nil) ##
def init_save
return false unless valid? return false unless valid?
set_expiration_date set_expiration_date
return false unless save return false unless save
UsersCredits::Manager.new(user: user).reset_credits UsersCredits::Manager.new(user: user).reset_credits
if invoice
@wallet_amount_debit = get_wallet_amount_debit
# debit wallet
wallet_transaction = debit_user_wallet
payment = if schedule
generate_schedule(operator_profile_id, payment_method, coupon_code)
else
generate_invoice(operator_profile_id, coupon_code, payment_intent_id)
end
if wallet_transaction
payment.wallet_amount = @wallet_amount_debit
payment.wallet_transaction_id = wallet_transaction.id
end
payment.save
end
true true
end end
def generate_schedule(operator_profile_id, payment_method, coupon_code = nil)
operator = InvoicingProfile.find(operator_profile_id)&.user
coupon = Coupon.find_by(code: coupon_code) unless coupon_code.nil?
PaymentScheduleService.new.create(
self,
plan.amount,
coupon: coupon,
operator: operator,
payment_method: payment_method,
user: user
)
end
def generate_invoice(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
coupon_id = nil
total = plan.amount
operator = InvoicingProfile.find(operator_profile_id)&.user
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
unless coupon_code.nil?
@coupon = Coupon.find_by(code: coupon_code)
unless @coupon.nil?
total = CouponService.new.apply(plan.amount, @coupon, user.id)
coupon_id = @coupon.id
end
end
invoice = Invoice.new(
invoiced_id: id,
invoiced_type: 'Subscription',
invoicing_profile: user.invoicing_profile,
statistic_profile: user.statistic_profile,
total: total,
coupon_id: coupon_id,
operator_profile_id: operator_profile_id,
stp_payment_intent_id: payment_intent_id,
payment_method: method
)
invoice.invoice_items.push InvoiceItem.new(
amount: plan.amount,
description: plan.name,
subscription_id: id
)
invoice
end
def generate_and_save_invoice(operator_profile_id) def generate_and_save_invoice(operator_profile_id)
generate_invoice(operator_profile_id).save generate_invoice(operator_profile_id).save
end end

View File

@ -59,4 +59,137 @@ class InvoicesService
end end
{ direction: direction, order_key: order_key } { direction: direction, order_key: order_key }
end end
##
# Create an Invoice with an associated array of InvoiceItem matching the given parameters
# @param payment_details {Hash} as generated by Price.compute
# @param operator_profile_id {Number} ID of the user that operates the invoice generation (may be an admin, a manager or the customer himself)
# @param reservation {Reservation} the booking reservation, if any
# @param subscription {Subscription} the booking subscription, if any
# @param payment_intent_id {String} ID of the Stripe::PaymentIntend, if the current invoice is paid by stripe
##
def self.create(payment_details, operator_profile_id, reservation: nil, subscription: nil, payment_intent_id: nil)
user = reservation&.user || subscription&.user
operator = InvoicingProfile.find(operator_profile_id)&.user
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
invoice = Invoice.new(
invoiced: subscription || reservation,
invoicing_profile: user.invoicing_profile,
statistic_profile: user.statistic_profile,
operator_profile_id: operator_profile_id,
stp_payment_intent_id: payment_intent_id,
payment_method: method
)
InvoicesService.generate_invoice_items(invoice, payment_details, reservation: reservation, subscription: subscription)
InvoicesService.set_total_and_coupon(invoice, user, payment_details[:coupon])
invoice
end
##
# Generate an array of {InvoiceItem} with the elements in provided reservation, price included.
# @param invoice {Invoice} the parent invoice
# @param payment_details {Hash} as generated by Price.compute
##
def self.generate_invoice_items(invoice, payment_details, reservation: nil, subscription: nil)
if reservation
case reservation.reservable
# === Event reservation ===
when Event
InvoicesService.generate_event_item(invoice, reservation, payment_details)
# === Space|Machine|Training reservation ===
else
InvoicesService.generate_generic_item(invoice, reservation, payment_details)
end
end
return unless subscription || reservation&.plan_id
subscription = reservation.generate_subscription if !subscription && reservation.plan_id
InvoicesService.generate_subscription_item(invoice, subscription, payment_details)
end
##
# Generate an InvoiceItem for each slot in the given reservation and save them in invoice.invoice_items.
# This method must be called if reservation.reservable is an Event
##
def self.generate_event_item(invoice, reservation, payment_details)
raise TypeError unless reservation.reservable.class == Event
reservation.slots.each do |slot|
description = "#{reservation.reservable.name}\n"
description += if slot.start_at.to_date != slot.end_at.to_date
I18n.t('events.from_STARTDATE_to_ENDDATE',
STARTDATE: I18n.l(slot.start_at.to_date, format: :long),
ENDDATE: I18n.l(slot.end_at.to_date, format: :long)) + ' ' +
I18n.t('events.from_STARTTIME_to_ENDTIME',
STARTTIME: I18n.l(slot.start_at, format: :hour_minute),
ENDTIME: I18n.l(slot.end_at, format: :hour_minute))
else
"#{I18n.l slot.start_at.to_date, format: :long} #{I18n.l slot.start_at, format: :hour_minute}" \
" - #{I18n.l slot.end_at, format: :hour_minute}"
end
price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] }
invoice.invoice_items.push InvoiceItem.new(
amount: price_slot[:price],
description: description
)
end
end
##
# Generate an InvoiceItem for each slot in the given reservation and save them in invoice.invoice_items.
# This method must be called if reservation.reservable is a Space, a Machine or a Training
##
def self.generate_generic_item(invoice, reservation, payment_details)
raise TypeError unless [Space, Machine, Training].include? reservation.reservable.class
reservation.slots.each do |slot|
description = reservation.reservable.name +
" #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] }
invoice.invoice_items.push InvoiceItem.new(
amount: price_slot[:price],
description: description
)
end
end
##
# Generate an InvoiceItem for the given subscription and save it in invoice.invoice_items.
# This method must be called only with a valid subscription
##
def self.generate_subscription_item(invoice, subscription, payment_details)
raise TypeError unless subscription
invoice.invoice_items.push InvoiceItem.new(
amount: payment_details[:elements][:plan],
description: subscription.plan.name,
subscription_id: subscription.id
)
end
##
# Set the total price to the reservation's invoice, summing its whole items.
# Additionally a coupon may be applied to this invoice to make a discount on the total price
# @param invoice {Invoice} the invoice to fill
# @param user {User} the customer
# @param [coupon] {Coupon} optional coupon to apply to the invoice
##
def self.set_total_and_coupon(invoice, user, coupon = nil)
return unless invoice
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
unless coupon.nil?
total = CouponService.new.apply(total, coupon, user.id)
invoice.coupon_id = coupon.id
end
invoice.total = total
end
end end

View File

@ -47,11 +47,13 @@ class PaymentScheduleService
end end
def create(subscription, total, coupon: nil, operator: nil, payment_method: nil, reservation: nil, user: nil) def create(subscription, total, coupon: nil, operator: nil, payment_method: nil, reservation: nil, user: nil)
subscription = reservation.generate_subscription if !subscription && reservation.plan_id
schedule = compute(subscription.plan, total, coupon) schedule = compute(subscription.plan, total, coupon)
ps = schedule[:payment_schedule] ps = schedule[:payment_schedule]
items = schedule[:items] items = schedule[:items]
ps.scheduled = subscription ps.scheduled = reservation || subscription
ps.payment_method = payment_method ps.payment_method = payment_method
ps.operator_profile = operator.invoicing_profile ps.operator_profile = operator.invoicing_profile
ps.invoicing_profile = user.invoicing_profile ps.invoicing_profile = user.invoicing_profile

View File

@ -9,9 +9,61 @@ class Reservations::Reserve
@operator_profile_id = operator_profile_id @operator_profile_id = operator_profile_id
end end
def pay_and_save(reservation, payment_details: nil, payment_intent_id: nil, schedule: false) ##
# TODO, pass the schedule payment up to subscription.save_with_payment(... schedule: schedule) # Confirm the payment of the given reservation, generate the associated documents and save teh record into
# the database.
##
def pay_and_save(reservation, payment_details: nil, payment_intent_id: nil, schedule: false, payment_method: nil)
user = User.find(user_id)
reservation.statistic_profile_id = StatisticProfile.find_by(user_id: user_id).id reservation.statistic_profile_id = StatisticProfile.find_by(user_id: user_id).id
reservation.save_with_payment(operator_profile_id, payment_details, payment_intent_id)
reservation.pre_check
payment = if schedule
generate_schedule(reservation: reservation,
total: payment_details[:before_coupon],
operator_profile_id: operator_profile_id,
user: user,
payment_method: payment_method,
coupon_code: payment_details[:coupon])
else
generate_invoice(reservation, operator_profile_id, payment_details, payment_intent_id)
end
payment.save
WalletService.debit_user_wallet(payment, user, reservation)
reservation.post_save
true
end end
private
##
# Generate the invoice for the given reservation+subscription
##
def generate_schedule(reservation: nil, total: nil, operator_profile_id: nil, user: nil, payment_method: nil, coupon_code: nil)
operator = InvoicingProfile.find(operator_profile_id)&.user
coupon = Coupon.find_by(code: coupon_code) unless coupon_code.nil?
PaymentScheduleService.new.create(
nil,
total,
coupon: coupon,
operator: operator,
payment_method: payment_method,
user: user,
reservation: reservation
)
end
##
# Generate the invoice for the given reservation
##
def generate_invoice(reservation, operator_profile_id, payment_details, payment_intent_id = nil)
InvoicesService.create(
payment_details,
operator_profile_id,
reservation: reservation,
payment_intent_id: payment_intent_id
)
end
end end

View File

@ -11,22 +11,31 @@ class Subscriptions::Subscribe
## ##
# @param subscription {Subscription} # @param subscription {Subscription}
# @param coupon {String} coupon code # @param payment_details {Hash} as generated by Price.compute
# @param invoice {Boolean}
# @param payment_intent_id {String} from stripe # @param payment_intent_id {String} from stripe
# @param schedule {Boolean} # @param schedule {Boolean}
# @param payment_method {String} only for schedules # @param payment_method {String} only for schedules
## ##
def pay_and_save(subscription, coupon: nil, invoice: false, payment_intent_id: nil, schedule: false, payment_method: nil) def pay_and_save(subscription, payment_details: nil, payment_intent_id: nil, schedule: false, payment_method: nil)
return false if user_id.nil? return false if user_id.nil?
subscription.statistic_profile_id = StatisticProfile.find_by(user_id: user_id).id subscription.statistic_profile_id = StatisticProfile.find_by(user_id: user_id).id
subscription.save_with_payment(operator_profile_id, subscription.init_save
invoice: invoice, user = User.find(user_id)
coupon_code: coupon,
payment_intent_id: payment_intent_id, payment = if schedule
schedule: schedule, generate_schedule(subscription: subscription,
payment_method: payment_method) total: payment_details[:before_coupon],
operator_profile_id: operator_profile_id,
user: user,
payment_method: payment_method,
coupon_code: payment_details[:coupon])
else
generate_invoice(subscription, operator_profile_id, payment_details, payment_intent_id)
end
payment.save
WalletService.debit_user_wallet(payment, user, subscription)
true
end end
def extend_subscription(subscription, new_expiration_date, free_days) def extend_subscription(subscription, new_expiration_date, free_days)
@ -38,10 +47,53 @@ class Subscriptions::Subscribe
expiration_date: new_expiration_date expiration_date: new_expiration_date
) )
if new_sub.save if new_sub.save
new_sub.user.generate_subscription_invoice(operator_profile_id) schedule = subscription.payment_schedule
details = Price.compute(true, new_sub.user, nil, [], plan_id: subscription.plan_id)
payment = if schedule
generate_schedule(subscription: new_sub,
total: details[:before_coupon],
operator_profile_id: operator_profile_id,
user: new_sub.user,
payment_method: schedule.payment_method)
else
generate_invoice(subscription, operator_profile_id, details)
end
payment.save
UsersCredits::Manager.new(user: new_sub.user).reset_credits UsersCredits::Manager.new(user: new_sub.user).reset_credits
return new_sub return new_sub
end end
false false
end end
private
##
# Generate the invoice for the given subscription
##
def generate_schedule(subscription: nil, total: nil, operator_profile_id: nil, user: nil, payment_method: nil, coupon_code: nil)
operator = InvoicingProfile.find(operator_profile_id)&.user
coupon = Coupon.find_by(code: coupon_code) unless coupon_code.nil?
PaymentScheduleService.new.create(
subscription,
total,
coupon: coupon,
operator: operator,
payment_method: payment_method,
user: user
)
end
##
# Generate the invoice for the given subscription
##
def generate_invoice(subscription, operator_profile_id, payment_details, payment_intent_id = nil)
InvoicesService.create(
payment_details,
operator_profile_id,
subscription: subscription,
payment_intent_id: payment_intent_id
)
end
end end

View File

@ -72,4 +72,34 @@ class WalletService
ii.invoice = avoir ii.invoice = avoir
ii.save! ii.save!
end end
##
# Compute the amount decreased from the user's wallet, if applicable
# @param payment {Invoice|PaymentSchedule}
# @param user {User} the customer
# @param coupon {Coupon|String} Coupon object or code
##
def self.wallet_amount_debit(payment, user, coupon = nil)
total = payment.total
total = CouponService.new.apply(total, coupon, user.id) if coupon
wallet_amount = (user.wallet.amount * 100).to_i
wallet_amount >= total ? total : wallet_amount
end
##
# Subtract the amount of the transactable item (Subscription|Reservation) from the customer's wallet
##
def self.debit_user_wallet(payment, user, transactable)
wallet_amount = WalletService.wallet_amount_debit(payment, user)
return unless wallet_amount.present? && wallet_amount != 0
amount = wallet_amount / 100.0
wallet_transaction = WalletService.new(user: user, wallet: user.wallet).debit(amount, transactable)
# wallet debit success
raise DebitWalletError unless wallet_transaction
payment.set_wallet_transaction(wallet_amount, wallet_transaction.id)
end
end end

View File

@ -53,7 +53,6 @@ class StripeWorker
{ name: object.name }, { name: object.name },
{ api_key: Setting.get('stripe_secret_key') } { api_key: Setting.get('stripe_secret_key') }
) )
p.product
else else
product = Stripe::Product.create( product = Stripe::Product.create(
{ {

View File

@ -10,7 +10,7 @@ require 'action_view/railtie'
require 'action_mailer/railtie' require 'action_mailer/railtie'
require 'active_job/railtie' require 'active_job/railtie'
# require 'action_cable/engine' # require 'action_cable/engine'
require 'rails/test_unit/railtie' if Rails.env.test? require 'rails/test_unit/railtie'
# require 'sprockets/railtie' # require 'sprockets/railtie'
require 'elasticsearch/rails/instrumentation' require 'elasticsearch/rails/instrumentation'
require 'elasticsearch/persistence/model' require 'elasticsearch/persistence/model'
@ -56,6 +56,7 @@ module Fablab
config.generators do |g| config.generators do |g|
g.orm :active_record g.orm :active_record
g.test_framework :mini_test
end end
if Rails.env.development? if Rails.env.development?

View File

@ -815,6 +815,7 @@ en:
credits_will_remain_unchanged: "The balance of free credits (training / machines / spaces) of the user will remain unchanged." credits_will_remain_unchanged: "The balance of free credits (training / machines / spaces) of the user will remain unchanged."
you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription: "You intentionally decide to extend the user's subscription by charging him again for his current subscription." you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription: "You intentionally decide to extend the user's subscription by charging him again for his current subscription."
credits_will_be_reset: "The balance of free credits (training / machines / spaces) of the user will be reset, unused credits will be lost." credits_will_be_reset: "The balance of free credits (training / machines / spaces) of the user will be reset, unused credits will be lost."
payment_scheduled: "If the previous subscription was charged through a payment schedule, this one will be charged the same way."
until_expiration_date: "Until (expiration date):" until_expiration_date: "Until (expiration date):"
you_successfully_changed_the_expiration_date_of_the_user_s_subscription: "You successfully changed the expiration date of the user's subscription" you_successfully_changed_the_expiration_date_of_the_user_s_subscription: "You successfully changed the expiration date of the user's subscription"
a_problem_occurred_while_saving_the_date: "A problem occurred while saving the date." a_problem_occurred_while_saving_the_date: "A problem occurred while saving the date."

View File

@ -815,6 +815,7 @@ fr:
credits_will_remain_unchanged: "Le solde de crédits gratuits (formations/machines/espaces) de l'utilisateur restera inchangé." credits_will_remain_unchanged: "Le solde de crédits gratuits (formations/machines/espaces) de l'utilisateur restera inchangé."
you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription: "Vous décidez délibérément d'étendre l'abonnement de l'utilisateur en lui faisant repayer le prix de l'abonnement qu'il possède actuellement." you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription: "Vous décidez délibérément d'étendre l'abonnement de l'utilisateur en lui faisant repayer le prix de l'abonnement qu'il possède actuellement."
credits_will_be_reset: "Le solde de crédits gratuits (formations/machines/espaces) de l'utilisateur sera remis à zéro, ses crédits non utilisés seront perdu." credits_will_be_reset: "Le solde de crédits gratuits (formations/machines/espaces) de l'utilisateur sera remis à zéro, ses crédits non utilisés seront perdu."
payment_scheduled: "Si l'abonnement précédent a été facturé via un échéancier de paiement mensualisé, celui-ci sera facturé de la même façon."
until_expiration_date: "Jusqu'à (date d'expiration) :" until_expiration_date: "Jusqu'à (date d'expiration) :"
you_successfully_changed_the_expiration_date_of_the_user_s_subscription: "Vous avez bien modifié la date d'expiration de l'abonnement de l'utilisateur" you_successfully_changed_the_expiration_date_of_the_user_s_subscription: "Vous avez bien modifié la date d'expiration de l'abonnement de l'utilisateur"
a_problem_occurred_while_saving_the_date: "Il y a eu un problème lors de l'enregistrement de la date." a_problem_occurred_while_saving_the_date: "Il y a eu un problème lors de l'enregistrement de la date."

View File

@ -1,5 +1,7 @@
# frozen_string_literal:true # frozen_string_literal:true
# From this migration, if the current Invoice is payed with Stripe, it will be stored in database
# using stp_payment_intent_id instead of stp_invoice_id
class AddStpPaymentIntentIdToInvoices < ActiveRecord::Migration[4.2] class AddStpPaymentIntentIdToInvoices < ActiveRecord::Migration[4.2]
def change def change
add_column :invoices, :stp_payment_intent_id, :string add_column :invoices, :stp_payment_intent_id, :string

View File

@ -9,6 +9,7 @@ class CreatePaymentScheduleItems < ActiveRecord::Migration[5.2]
t.jsonb :details, default: '{}' t.jsonb :details, default: '{}'
t.belongs_to :payment_schedule, foreign_key: true t.belongs_to :payment_schedule, foreign_key: true
t.belongs_to :invoice, foreign_key: true t.belongs_to :invoice, foreign_key: true
t.string :footprint
t.timestamps t.timestamps
end end

View File

@ -108,8 +108,8 @@ SET default_tablespace = '';
CREATE TABLE public.abuses ( CREATE TABLE public.abuses (
id integer NOT NULL, id integer NOT NULL,
signaled_id integer,
signaled_type character varying, signaled_type character varying,
signaled_id integer,
first_name character varying, first_name character varying,
last_name character varying, last_name character varying,
email character varying, email character varying,
@ -187,8 +187,8 @@ CREATE TABLE public.addresses (
locality character varying, locality character varying,
country character varying, country character varying,
postal_code character varying, postal_code character varying,
placeable_id integer,
placeable_type character varying, placeable_type character varying,
placeable_id integer,
created_at timestamp without time zone, created_at timestamp without time zone,
updated_at timestamp without time zone updated_at timestamp without time zone
); );
@ -263,8 +263,8 @@ CREATE TABLE public.ar_internal_metadata (
CREATE TABLE public.assets ( CREATE TABLE public.assets (
id integer NOT NULL, id integer NOT NULL,
viewable_id integer,
viewable_type character varying, viewable_type character varying,
viewable_id integer,
attachment character varying, attachment character varying,
type character varying, type character varying,
created_at timestamp without time zone, created_at timestamp without time zone,
@ -504,8 +504,8 @@ ALTER SEQUENCE public.coupons_id_seq OWNED BY public.coupons.id;
CREATE TABLE public.credits ( CREATE TABLE public.credits (
id integer NOT NULL, id integer NOT NULL,
creditable_id integer,
creditable_type character varying, creditable_type character varying,
creditable_id integer,
plan_id integer, plan_id integer,
hours integer, hours integer,
created_at timestamp without time zone, created_at timestamp without time zone,
@ -1046,8 +1046,8 @@ ALTER SEQUENCE public.invoice_items_id_seq OWNED BY public.invoice_items.id;
CREATE TABLE public.invoices ( CREATE TABLE public.invoices (
id integer NOT NULL, id integer NOT NULL,
invoiced_id integer,
invoiced_type character varying, invoiced_type character varying,
invoiced_id integer,
stp_invoice_id character varying, stp_invoice_id character varying,
total integer, total integer,
created_at timestamp without time zone, created_at timestamp without time zone,
@ -1227,15 +1227,15 @@ ALTER SEQUENCE public.machines_id_seq OWNED BY public.machines.id;
CREATE TABLE public.notifications ( CREATE TABLE public.notifications (
id integer NOT NULL, id integer NOT NULL,
receiver_id integer, receiver_id integer,
attached_object_id integer,
attached_object_type character varying, attached_object_type character varying,
attached_object_id integer,
notification_type_id integer, notification_type_id integer,
is_read boolean DEFAULT false, is_read boolean DEFAULT false,
created_at timestamp without time zone, created_at timestamp without time zone,
updated_at timestamp without time zone, updated_at timestamp without time zone,
receiver_type character varying, receiver_type character varying,
is_send boolean DEFAULT false, is_send boolean DEFAULT false,
meta_data jsonb DEFAULT '{}'::jsonb meta_data jsonb DEFAULT '"{}"'::jsonb
); );
@ -1473,6 +1473,7 @@ CREATE TABLE public.payment_schedule_items (
details jsonb DEFAULT '"{}"'::jsonb, details jsonb DEFAULT '"{}"'::jsonb,
payment_schedule_id bigint, payment_schedule_id bigint,
invoice_id bigint, invoice_id bigint,
footprint character varying,
created_at timestamp without time zone NOT NULL, created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL updated_at timestamp without time zone NOT NULL
); );
@ -1656,8 +1657,8 @@ CREATE TABLE public.prices (
id integer NOT NULL, id integer NOT NULL,
group_id integer, group_id integer,
plan_id integer, plan_id integer,
priceable_id integer,
priceable_type character varying, priceable_type character varying,
priceable_id integer,
amount integer, amount integer,
created_at timestamp without time zone NOT NULL, created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL updated_at timestamp without time zone NOT NULL
@ -1972,8 +1973,8 @@ CREATE TABLE public.reservations (
message text, message text,
created_at timestamp without time zone, created_at timestamp without time zone,
updated_at timestamp without time zone, updated_at timestamp without time zone,
reservable_id integer,
reservable_type character varying, reservable_type character varying,
reservable_id integer,
nb_reserve_places integer, nb_reserve_places integer,
statistic_profile_id integer statistic_profile_id integer
); );
@ -2005,8 +2006,8 @@ ALTER SEQUENCE public.reservations_id_seq OWNED BY public.reservations.id;
CREATE TABLE public.roles ( CREATE TABLE public.roles (
id integer NOT NULL, id integer NOT NULL,
name character varying, name character varying,
resource_id integer,
resource_type character varying, resource_type character varying,
resource_id integer,
created_at timestamp without time zone, created_at timestamp without time zone,
updated_at timestamp without time zone updated_at timestamp without time zone
); );
@ -2942,8 +2943,8 @@ CREATE TABLE public.users_roles (
CREATE TABLE public.wallet_transactions ( CREATE TABLE public.wallet_transactions (
id integer NOT NULL, id integer NOT NULL,
wallet_id integer, wallet_id integer,
transactable_id integer,
transactable_type character varying, transactable_type character varying,
transactable_id integer,
transaction_type character varying, transaction_type character varying,
amount integer, amount integer,
created_at timestamp without time zone NOT NULL, created_at timestamp without time zone NOT NULL,
@ -4032,6 +4033,14 @@ ALTER TABLE ONLY public.roles
ADD CONSTRAINT roles_pkey PRIMARY KEY (id); ADD CONSTRAINT roles_pkey PRIMARY KEY (id);
--
-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.schema_migrations
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
-- --
-- Name: settings settings_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: settings settings_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
@ -5096,29 +5105,6 @@ CREATE INDEX profiles_lower_unaccent_last_name_trgm_idx ON public.profiles USING
CREATE INDEX projects_search_vector_idx ON public.projects USING gin (search_vector); CREATE INDEX projects_search_vector_idx ON public.projects USING gin (search_vector);
--
-- Name: unique_schema_migrations; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX unique_schema_migrations ON public.schema_migrations USING btree (version);
--
-- Name: accounting_periods accounting_periods_del_protect; Type: RULE; Schema: public; Owner: -
--
CREATE RULE accounting_periods_del_protect AS
ON DELETE TO public.accounting_periods DO INSTEAD NOTHING;
--
-- Name: accounting_periods accounting_periods_upd_protect; Type: RULE; Schema: public; Owner: -
--
CREATE RULE accounting_periods_upd_protect AS
ON UPDATE TO public.accounting_periods DO INSTEAD NOTHING;
-- --
-- Name: projects projects_search_content_trigger; Type: TRIGGER; Schema: public; Owner: - -- Name: projects projects_search_content_trigger; Type: TRIGGER; Schema: public; Owner: -
-- --
@ -5653,7 +5639,6 @@ INSERT INTO "schema_migrations" (version) VALUES
('20140605125131'), ('20140605125131'),
('20140605142133'), ('20140605142133'),
('20140605151442'), ('20140605151442'),
('20140606133116'),
('20140609092700'), ('20140609092700'),
('20140609092827'), ('20140609092827'),
('20140610153123'), ('20140610153123'),
@ -5722,14 +5707,12 @@ INSERT INTO "schema_migrations" (version) VALUES
('20150507075620'), ('20150507075620'),
('20150512123546'), ('20150512123546'),
('20150520132030'), ('20150520132030'),
('20150520133409'),
('20150526130729'), ('20150526130729'),
('20150527153312'), ('20150527153312'),
('20150529113555'), ('20150529113555'),
('20150601125944'), ('20150601125944'),
('20150603104502'), ('20150603104502'),
('20150603104658'), ('20150603104658'),
('20150603133050'),
('20150604081757'), ('20150604081757'),
('20150604131525'), ('20150604131525'),
('20150608142234'), ('20150608142234'),
@ -5811,7 +5794,6 @@ INSERT INTO "schema_migrations" (version) VALUES
('20160905142700'), ('20160905142700'),
('20160906094739'), ('20160906094739'),
('20160906094847'), ('20160906094847'),
('20160906145713'),
('20160915105234'), ('20160915105234'),
('20161123104604'), ('20161123104604'),
('20170109085345'), ('20170109085345'),

View File

@ -100,7 +100,7 @@ value_history_10:
id: 10 id: 10
setting_id: 10 setting_id: 10
invoicing_profile_id: 1 invoicing_profile_id: 1
value: YYMMmmmX[/VL]R[/A]S[/E] value: YYMMmmmX[/VL]R[/A]
created_at: 2018-12-17 11:23:01.603733000 Z created_at: 2018-12-17 11:23:01.603733000 Z
updated_at: 2018-12-17 11:23:01.603733000 Z updated_at: 2018-12-17 11:23:01.603733000 Z
footprint: ed23a2eb1903befc977621bc3c3b19aad831fe550ebaa99e9299238b3d93c275 footprint: ed23a2eb1903befc977621bc3c3b19aad831fe550ebaa99e9299238b3d93c275
@ -693,3 +693,11 @@ history_value_73:
value: true value: true
created_at: 2020-06-17 10:48:19.002417000 Z created_at: 2020-06-17 10:48:19.002417000 Z
updated_at: 2020-06-17 10:48:19.002417000 Z updated_at: 2020-06-17 10:48:19.002417000 Z
value_history_74:
id: 74
setting_id: 10
invoicing_profile_id: 1
value: YYMMmmmX[/VL]R[/A]S[/E]
created_at: 2020-12-14 14:37:35.615124000 Z
updated_at: 2020-12-14 14:37:35.615124000 Z

View File

@ -22,6 +22,7 @@ machine_1:
created_at: 2016-04-04 14:11:34.210242000 Z created_at: 2016-04-04 14:11:34.210242000 Z
updated_at: 2016-04-04 14:11:34.210242000 Z updated_at: 2016-04-04 14:11:34.210242000 Z
slug: decoupeuse-laser slug: decoupeuse-laser
stp_product_id: prod_IZPyHpMCl38iQl
machine_2: machine_2:
id: 2 id: 2
@ -38,6 +39,7 @@ machine_2:
created_at: 2016-04-04 14:11:34.274025000 Z created_at: 2016-04-04 14:11:34.274025000 Z
updated_at: 2016-04-04 14:11:34.274025000 Z updated_at: 2016-04-04 14:11:34.274025000 Z
slug: decoupeuse-vinyle slug: decoupeuse-vinyle
stp_product_id: prod_IZPyPShaaRgSML
machine_3: machine_3:
id: 3 id: 3
@ -54,6 +56,7 @@ machine_3:
created_at: 2016-04-04 14:11:34.304247000 Z created_at: 2016-04-04 14:11:34.304247000 Z
updated_at: 2016-04-04 14:11:34.304247000 Z updated_at: 2016-04-04 14:11:34.304247000 Z
slug: shopbot-grande-fraiseuse slug: shopbot-grande-fraiseuse
stp_product_id: prod_IZPyEjmdfMowhY
machine_4: machine_4:
id: 4 id: 4
@ -67,6 +70,7 @@ machine_4:
created_at: 2001-01-01 14:11:34.341810000 Z created_at: 2001-01-01 14:11:34.341810000 Z
updated_at: 2001-01-01 14:11:34.341810000 Z updated_at: 2001-01-01 14:11:34.341810000 Z
slug: imprimante-3d slug: imprimante-3d
stp_product_id: prod_IZPy85vZOQpAo5
machine_5: machine_5:
id: 5 id: 5
@ -89,6 +93,7 @@ machine_5:
created_at: 2016-04-04 14:11:34.379481000 Z created_at: 2016-04-04 14:11:34.379481000 Z
updated_at: 2016-04-04 14:11:34.379481000 Z updated_at: 2016-04-04 14:11:34.379481000 Z
slug: petite-fraiseuse slug: petite-fraiseuse
stp_product_id: prod_IZPyBJEgbcpWMC
machine_6: machine_6:
id: 6 id: 6
@ -123,3 +128,4 @@ machine_6:
created_at: 2016-04-04 14:11:34.424740000 Z created_at: 2016-04-04 14:11:34.424740000 Z
updated_at: 2016-04-04 14:11:34.424740000 Z updated_at: 2016-04-04 14:11:34.424740000 Z
slug: form1-imprimante-3d slug: form1-imprimante-3d
stp_product_id: prod_IZPyjCzvLmLWAz

View File

@ -15,6 +15,8 @@ plan_1:
base_name: Mensuel base_name: Mensuel
ui_weight: 1 ui_weight: 1
interval_count: 1 interval_count: 1
slug: mensuel
stp_product_id: prod_IZPyXhfyNiGkWR
plan_2: plan_2:
id: 2 id: 2
@ -32,6 +34,8 @@ plan_2:
base_name: Sleede base_name: Sleede
ui_weight: 5 ui_weight: 5
interval_count: 2 interval_count: 2
slug: sleede
stp_product_id: prod_IZPykam7a4satn
plan_3: plan_3:
id: 3 id: 3
@ -49,4 +53,26 @@ plan_3:
type: Plan type: Plan
base_name: Mensuel tarif réduit base_name: Mensuel tarif réduit
ui_weight: 0 ui_weight: 0
interval_count: 1*
slug: mensuel-tarif-reduit
stp_product_id: prod_IZPyM4N36h86G0
plan_schedulable:
id: 4
name: Abonnement mensualisable - standard, association, year
amount: 113600
interval: year
group_id: 1
stp_plan_id:
created_at: 2020-12-14 14:10:11.056241000 Z
updated_at: 2020-12-14 14:10:11.137421000 Z
training_credit_nb: 1
is_rolling: true
description:
type: Plan
base_name: Abonnement mensualisable
ui_weight: 10
interval_count: 1 interval_count: 1
monthly_payment: true
slug: abonnement-mensualisable
stp_product_id: prod_IZQAhb9nLu4jfN

View File

@ -7,3 +7,4 @@ space_1:
created_at: 2017-02-15 15:55:04.123928000 Z created_at: 2017-02-15 15:55:04.123928000 Z
updated_at: 2017-02-15 15:55:04.123928000 Z updated_at: 2017-02-15 15:55:04.123928000 Z
characteristics: Scie à chantourner, rabot, dégauchisseuse, chanfreineuse et pyrograveur characteristics: Scie à chantourner, rabot, dégauchisseuse, chanfreineuse et pyrograveur
stp_product_id: prod_IZPyHjIb2owoB8

View File

@ -7,6 +7,7 @@ training_1:
nb_total_places: nb_total_places:
slug: formation-imprimante-3d slug: formation-imprimante-3d
description: description:
stp_product_id: prod_IZPyXw6BDBBFOg
training_2: training_2:
id: 2 id: 2
@ -16,6 +17,7 @@ training_2:
nb_total_places: nb_total_places:
slug: formation-laser-vinyle slug: formation-laser-vinyle
description: description:
stp_product_id: prod_IZPytTl1wSB5jH
training_3: training_3:
id: 3 id: 3
@ -25,6 +27,7 @@ training_3:
nb_total_places: nb_total_places:
slug: formation-petite-fraiseuse-numerique slug: formation-petite-fraiseuse-numerique
description: description:
stp_product_id: prod_IZPyAA1A4QfEyL
training_4: training_4:
id: 4 id: 4
@ -34,6 +37,7 @@ training_4:
nb_total_places: nb_total_places:
slug: formation-shopbot-grande-fraiseuse slug: formation-shopbot-grande-fraiseuse
description: description:
stp_product_id: prod_IZPyU27NjDSmqB
training_5: training_5:
id: 5 id: 5
@ -43,3 +47,4 @@ training_5:
nb_total_places: nb_total_places:
slug: formation-logiciel-2d slug: formation-logiciel-2d
description: description:
stp_product_id: prod_IZPyvdgQHMByB3

View File

@ -1,82 +1,471 @@
# frozen_string_literal: true # frozen_string_literal: true
module Reservations require 'test_helper'
class CreateAsAdminTest < ActionDispatch::IntegrationTest
setup do
@user_without_subscription = User.members.without_subscription.first
@user_with_subscription = User.members.with_subscription.second
@admin = User.with_role(:admin).first
login_as(@admin, scope: :user)
end
test 'user without subscription reserves a machine with success' do class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest
machine = Machine.find(6) setup do
availability = machine.availabilities.first @user_without_subscription = User.members.without_subscription.first
@user_with_subscription = User.members.with_subscription.second
@admin = User.with_role(:admin).first
login_as(@admin, scope: :user)
end
reservations_count = Reservation.count test 'user without subscription reserves a machine with success' do
invoice_count = Invoice.count machine = Machine.find(6)
invoice_items_count = InvoiceItem.count availability = machine.availabilities.first
users_credit_count = UsersCredit.count
post reservations_path, params: { reservation: {
user_id: @user_without_subscription.id,
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 1, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: nil).amount, invoice_item.amount
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user without subscription reserves a training with success' do
training = Training.first
availability = training.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
post reservations_path, params: { reservation: {
user_id: @user_without_subscription.id,
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 1, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: nil).amount, invoice_item.amount
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user without subscription reserves a training with success' do
training = Training.first
availability = training.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
post reservations_path, params: { reservation: {
user_id: @user_without_subscription.id,
reservable_id: training.id,
reservable_type: training.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 1, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
# invoice_items
invoice_item = InvoiceItem.last
assert_equal invoice_item.amount, training.amount_by_group(@user_without_subscription.group_id).amount
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user with subscription reserves a machine with success' do
plan = @user_with_subscription.subscribed_plan
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
post reservations_path, params: { reservation: {
user_id: @user_with_subscription.id,
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
},
{
start_at: (availability.start_at + 1.hour).to_s(:iso8601),
end_at: (availability.start_at + 2.hours).to_s(:iso8601),
availability_id: availability.id
}
]
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count + 1, UsersCredit.count
# subscription assertions
assert_equal 1, @user_with_subscription.subscriptions.count
assert_not_nil @user_with_subscription.subscribed_plan
assert_equal plan.id, @user_with_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 2, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
# invoice_items assertions
invoice_items = InvoiceItem.last(2)
machine_price = machine.prices.find_by(group_id: @user_with_subscription.group_id, plan_id: plan.id).amount
assert(invoice_items.any? { |ii| ii.amount.zero? })
assert(invoice_items.any? { |ii| ii.amount == machine_price })
# users_credits assertions
users_credit = UsersCredit.last
assert_equal @user_with_subscription, users_credit.user
assert_equal [reservation.slots.count, plan.machine_credits.find_by(creditable_id: machine.id).hours].min, users_credit.hours_used
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user without subscription reserves a machine and pay by wallet with success' do
@vlonchamp = User.find_by(username: 'vlonchamp')
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
post reservations_path, params: { reservation: {
user_id: @vlonchamp.id,
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 1, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal machine.prices.find_by(group_id: @vlonchamp.group_id, plan_id: nil).amount, invoice_item.amount
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal @vlonchamp.wallet.amount, 0
assert_equal @vlonchamp.wallet.wallet_transactions.count, 2
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal transaction.transaction_type, 'debit'
assert_equal transaction.amount, 10
assert_equal transaction.amount, invoice.wallet_amount / 100.0
assert_equal transaction.id, invoice.wallet_transaction_id
end
test 'user reserves a machine and a subscription pay by wallet with success' do
@vlonchamp = User.find_by(username: 'vlonchamp')
machine = Machine.find(6)
availability = machine.availabilities.first
plan = Plan.find_by(group_id: @vlonchamp.group.id, type: 'Plan', base_name: 'Mensuel tarif réduit')
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
wallet_transactions_count = WalletTransaction.count
post reservations_path, params: { reservation: {
user_id: @vlonchamp.id,
reservable_id: machine.id,
reservable_type: machine.class.name,
plan_id: plan.id,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count + 1, UsersCredit.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# subscription assertions
assert_equal 1, @vlonchamp.subscriptions.count
assert_not_nil @vlonchamp.subscribed_plan
assert_equal plan.id, @vlonchamp.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 2, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert_equal invoice.total, 2000
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal @vlonchamp.wallet.amount, 0
assert_equal @vlonchamp.wallet.wallet_transactions.count, 2
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal transaction.transaction_type, 'debit'
assert_equal transaction.amount, 10
assert_equal transaction.amount, invoice.wallet_amount / 100.0
assert_equal transaction.id, invoice.wallet_transaction_id
end
test 'user without subscription reserves a machine and pay wallet with success' do
@vlonchamp = User.find_by(username: 'vlonchamp')
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
post reservations_path, params: { reservation: {
user_id: @vlonchamp.id,
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
# subscription assertions
assert_equal 0, @vlonchamp.subscriptions.count
assert_nil @vlonchamp.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert_not_nil reservation.invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user reserves a training and a subscription with success' do
training = Training.first
availability = training.availabilities.first
plan = Plan.where(group_id: @user_without_subscription.group.id, type: 'Plan').first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
post reservations_path, params: { reservation: {
user_id: @user_without_subscription.id,
plan_id: plan.id,
reservable_id: training.id,
reservable_type: training.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
offered: false,
availability_id: availability.id
}
]
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal Mime[:json], response.content_type
result = json_response(response.body)
# Check the DB objects have been created as they should
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count + 1, UsersCredit.count
# subscription assertions
assert_equal 1, @user_without_subscription.subscriptions.count
assert_not_nil @user_without_subscription.subscribed_plan
assert_equal plan.id, @user_without_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.find(result[:id])
assert reservation.invoice
assert_equal 2, reservation.invoice.invoice_items.count
# credits assertions
assert_equal 1, @user_without_subscription.credits.count
assert_equal 'Training', @user_without_subscription.credits.last.creditable_type
assert_equal training.id, @user_without_subscription.credits.last.creditable_id
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert_equal plan.amount, invoice.total
# invoice_items
invoice_items = InvoiceItem.last(2)
assert(invoice_items.any? { |ii| ii.amount == plan.amount && !ii.subscription_id.nil? })
assert(invoice_items.any? { |ii| ii.amount.zero? })
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user reserves a training and a subscription with payment schedule' do
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
subscriptions_count = Subscription.count
users_credit_count = UsersCredit.count
payment_schedule_count = PaymentSchedule.count
payment_schedule_items_count = PaymentScheduleItem.count
training = Training.find(1)
availability = training.availabilities.first
plan = Plan.find_by(group_id: @user_without_subscription.group.id, type: 'Plan', base_name: 'Abonnement mensualisable')
VCR.use_cassette('reservations_admin_training_subscription_with_payment_schedule') do
post reservations_path, params: { reservation: { post reservations_path, params: { reservation: {
user_id: @user_without_subscription.id, user_id: @user_without_subscription.id,
payment_method: '', # pay by check
reservable_id: training.id, reservable_id: training.id,
reservable_type: training.class.name, reservable_type: training.class.name,
slots_attributes: [ slots_attributes: [
@ -85,367 +474,31 @@ module Reservations
end_at: (availability.start_at + 1.hour).to_s(:iso8601), end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id availability_id: availability.id
} }
] ],
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 1, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
# invoice_items
invoice_item = InvoiceItem.last
assert_equal invoice_item.amount, training.amount_by_group(@user_without_subscription.group_id).amount
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user with subscription reserves a machine with success' do
plan = @user_with_subscription.subscribed_plan
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
post reservations_path, params: { reservation: {
user_id: @user_with_subscription.id,
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
},
{
start_at: (availability.start_at + 1.hour).to_s(:iso8601),
end_at: (availability.start_at + 2.hours).to_s(:iso8601),
availability_id: availability.id
}
]
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count + 1, UsersCredit.count
# subscription assertions
assert_equal 1, @user_with_subscription.subscriptions.count
assert_not_nil @user_with_subscription.subscribed_plan
assert_equal plan.id, @user_with_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 2, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
# invoice_items assertions
invoice_items = InvoiceItem.last(2)
machine_price = machine.prices.find_by(group_id: @user_with_subscription.group_id, plan_id: plan.id).amount
assert(invoice_items.any? { |ii| ii.amount.zero? })
assert(invoice_items.any? { |ii| ii.amount == machine_price })
# users_credits assertions
users_credit = UsersCredit.last
assert_equal @user_with_subscription, users_credit.user
assert_equal [reservation.slots.count, plan.machine_credits.find_by(creditable_id: machine.id).hours].min, users_credit.hours_used
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user without subscription reserves a machine and pay by wallet with success' do
@vlonchamp = User.find_by(username: 'vlonchamp')
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
post reservations_path, params: { reservation: {
user_id: @vlonchamp.id,
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 1, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal machine.prices.find_by(group_id: @vlonchamp.group_id, plan_id: nil).amount, invoice_item.amount
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal @vlonchamp.wallet.amount, 0
assert_equal @vlonchamp.wallet.wallet_transactions.count, 2
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal transaction.transaction_type, 'debit'
assert_equal transaction.amount, 10
assert_equal transaction.amount, invoice.wallet_amount / 100.0
assert_equal transaction.id, invoice.wallet_transaction_id
end
test 'user reserves a machine and a subscription pay by wallet with success' do
@vlonchamp = User.find_by(username: 'vlonchamp')
machine = Machine.find(6)
availability = machine.availabilities.first
plan = Plan.find_by(group_id: @vlonchamp.group.id, type: 'Plan', base_name: 'Mensuel tarif réduit')
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
wallet_transactions_count = WalletTransaction.count
post reservations_path, params: { reservation: {
user_id: @vlonchamp.id,
reservable_id: machine.id,
reservable_type: machine.class.name,
plan_id: plan.id, plan_id: plan.id,
slots_attributes: [ payment_schedule: true
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
} }.to_json, headers: default_headers } }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count + 1, UsersCredit.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# subscription assertions
assert_equal 1, @vlonchamp.subscriptions.count
assert_not_nil @vlonchamp.subscribed_plan
assert_equal plan.id, @vlonchamp.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 2, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert_equal invoice.total, 2000
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal @vlonchamp.wallet.amount, 0
assert_equal @vlonchamp.wallet.wallet_transactions.count, 2
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal transaction.transaction_type, 'debit'
assert_equal transaction.amount, 10
assert_equal transaction.amount, invoice.wallet_amount / 100.0
assert_equal transaction.id, invoice.wallet_transaction_id
end end
test 'user without subscription reserves a machine and pay wallet with success' do # Check response format & status
@vlonchamp = User.find_by(username: 'vlonchamp') assert_equal 201, response.status, response.body
machine = Machine.find(6) assert_equal Mime[:json], response.content_type
availability = machine.availabilities.first assert_equal reservations_count + 1, Reservation.count, 'missing the reservation'
assert_equal invoice_count, Invoice.count, "an invoice was generated but it shouldn't"
assert_equal invoice_items_count, InvoiceItem.count, "some invoice items were generated but they shouldn't"
assert_equal users_credit_count, UsersCredit.count, "user's credits count has changed but it shouldn't"
assert_equal subscriptions_count + 1, Subscription.count, 'missing the subscription'
assert_equal payment_schedule_count + 1, PaymentSchedule.count, 'missing the payment schedule'
assert_equal payment_schedule_items_count + 12, PaymentScheduleItem.count, 'missing some payment schedule items'
reservations_count = Reservation.count # subscription assertions
invoice_count = Invoice.count assert_equal 1, @user_without_subscription.subscriptions.count
invoice_items_count = InvoiceItem.count assert_not_nil @user_without_subscription.subscribed_plan, "user's subscribed plan was not found"
users_credit_count = UsersCredit.count assert_not_nil @user_without_subscription.subscription, "user's subscription was not found"
assert_equal plan.id, @user_without_subscription.subscribed_plan.id, "user's plan does not match"
post reservations_path, params: { reservation: { # Check the answer
user_id: @vlonchamp.id, reservation = json_response(response.body)
reservable_id: machine.id, assert_equal plan.id, reservation[:user][:subscribed_plan][:id], 'subscribed plan does not match'
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
# subscription assertions
assert_equal 0, @vlonchamp.subscriptions.count
assert_nil @vlonchamp.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert_not_nil reservation.invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user reserves a training and a subscription with success' do
training = Training.first
availability = training.availabilities.first
plan = Plan.where(group_id: @user_without_subscription.group.id, type: 'Plan').first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
post reservations_path, params: { reservation: {
user_id: @user_without_subscription.id,
plan_id: plan.id,
reservable_id: training.id,
reservable_type: training.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
offered: false,
availability_id: availability.id
}
]
} }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal Mime[:json], response.content_type
result = json_response(response.body)
# Check the DB objects have been created as they should
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count + 1, UsersCredit.count
# subscription assertions
assert_equal 1, @user_without_subscription.subscriptions.count
assert_not_nil @user_without_subscription.subscribed_plan
assert_equal plan.id, @user_without_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.find(result[:id])
assert reservation.invoice
assert_equal 2, reservation.invoice.invoice_items.count
# credits assertions
assert_equal 1, @user_without_subscription.credits.count
assert_equal 'Training', @user_without_subscription.credits.last.creditable_type
assert_equal training.id, @user_without_subscription.credits.last.creditable_id
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert_equal plan.amount, invoice.total
# invoice_items
invoice_items = InvoiceItem.last(2)
assert(invoice_items.any? { |ii| ii.amount == plan.amount && !ii.subscription_id.nil? })
assert(invoice_items.any? { |ii| ii.amount.zero? })
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
end end
end end

View File

@ -2,299 +2,147 @@
require 'test_helper' require 'test_helper'
module Reservations class Reservations::CreateTest < ActionDispatch::IntegrationTest
class CreateTest < ActionDispatch::IntegrationTest setup do
setup do @user_without_subscription = User.members.without_subscription.first
@user_without_subscription = User.members.without_subscription.first @user_with_subscription = User.members.with_subscription.second
@user_with_subscription = User.members.with_subscription.second end
end
test 'user without subscription reserves a machine with success' do test 'user without subscription reserves a machine with success' do
login_as(@user_without_subscription, scope: :user) login_as(@user_without_subscription, scope: :user)
machine = Machine.find(6) machine = Machine.find(6)
availability = machine.availabilities.first availability = machine.availabilities.first
reservations_count = Reservation.count reservations_count = Reservation.count
invoice_count = Invoice.count invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count users_credit_count = UsersCredit.count
subscriptions_count = Subscription.count subscriptions_count = Subscription.count
VCR.use_cassette('reservations_create_for_machine_without_subscription_success') do VCR.use_cassette('reservations_create_for_machine_without_subscription_success') do
post '/api/payments/confirm_payment', post '/api/payments/confirm_payment',
params: { params: {
payment_method_id: stripe_payment_method, payment_method_id: stripe_payment_method,
cart_items: { cart_items: {
reservation: { reservation: {
reservable_id: machine.id, reservable_id: machine.id,
reservable_type: machine.class.name, reservable_type: machine.class.name,
slots_attributes: [ slots_attributes: [
{ {
start_at: availability.start_at.to_s(:iso8601), start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601), end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id availability_id: availability.id
} }
] ]
}
} }
}.to_json, headers: default_headers }
end }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal subscriptions_count, Subscription.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 1, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
refute invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: nil).amount, invoice_item.amount
assert invoice_item.check_footprint
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end end
test 'user without subscription reserves a machine with error' do # general assertions
login_as(@user_without_subscription, scope: :user) assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal subscriptions_count, Subscription.count
machine = Machine.find(6) # subscription assertions
availability = machine.availabilities.first assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
reservations_count = Reservation.count # reservation assertions
invoice_count = Invoice.count reservation = Reservation.last
invoice_items_count = InvoiceItem.count
notifications_count = Notification.count
VCR.use_cassette('reservations_create_for_machine_without_subscription_error') do assert reservation.invoice
post '/api/payments/confirm_payment', assert_equal 1, reservation.invoice.invoice_items.count
params: {
payment_method_id: stripe_payment_method(error: :card_declined), # invoice assertions
cart_items: { invoice = reservation.invoice
reservation: {
reservable_id: machine.id, refute invoice.stp_payment_intent_id.blank?
reservable_type: machine.class.name, refute invoice.total.blank?
slots_attributes: [ assert invoice.check_footprint
{
start_at: availability.start_at.to_s(:iso8601), # invoice_items assertions
end_at: (availability.start_at + 1.hour).to_s(:iso8601), invoice_item = InvoiceItem.last
availability_id: availability.id
} assert_equal machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: nil).amount, invoice_item.amount
] assert invoice_item.check_footprint
}
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user without subscription reserves a machine with error' do
login_as(@user_without_subscription, scope: :user)
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
notifications_count = Notification.count
VCR.use_cassette('reservations_create_for_machine_without_subscription_error') do
post '/api/payments/confirm_payment',
params: {
payment_method_id: stripe_payment_method(error: :card_declined),
cart_items: {
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
} }
}.to_json, headers: default_headers }
end }.to_json, headers: default_headers
# Check response format & status
assert_equal 200, response.status, "API does not return the expected status. #{response.body}"
assert_equal Mime[:json], response.content_type
# Check the error was handled
assert_match /Your card was declined/, response.body
# Check the subscription wasn't taken
assert_equal reservations_count, Reservation.count
assert_equal invoice_count, Invoice.count
assert_equal invoice_items_count, InvoiceItem.count
assert_equal notifications_count, Notification.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
end end
test 'user without subscription reserves a training with success' do # Check response format & status
login_as(@user_without_subscription, scope: :user) assert_equal 200, response.status, "API does not return the expected status. #{response.body}"
assert_equal Mime[:json], response.content_type
training = Training.first # Check the error was handled
availability = training.availabilities.first assert_match /Your card was declined/, response.body
reservations_count = Reservation.count # Check the subscription wasn't taken
invoice_count = Invoice.count assert_equal reservations_count, Reservation.count
invoice_items_count = InvoiceItem.count assert_equal invoice_count, Invoice.count
assert_equal invoice_items_count, InvoiceItem.count
assert_equal notifications_count, Notification.count
VCR.use_cassette('reservations_create_for_training_without_subscription_success') do # subscription assertions
post '/api/payments/confirm_payment', assert_equal 0, @user_without_subscription.subscriptions.count
params: { assert_nil @user_without_subscription.subscribed_plan
payment_method_id: stripe_payment_method, end
cart_items: {
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: availability.end_at.to_s(:iso8601),
availability_id: availability.id
}
]
}
}
}.to_json, headers: default_headers
end
# general assertions test 'user without subscription reserves a training with success' do
assert_equal 201, response.status login_as(@user_without_subscription, scope: :user)
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
# subscription assertions training = Training.first
assert_equal 0, @user_without_subscription.subscriptions.count availability = training.availabilities.first
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions reservations_count = Reservation.count
reservation = Reservation.last invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
assert reservation.invoice VCR.use_cassette('reservations_create_for_training_without_subscription_success') do
assert_equal 1, reservation.invoice.invoice_items.count post '/api/payments/confirm_payment',
params: {
# invoice assertions payment_method_id: stripe_payment_method,
invoice = reservation.invoice cart_items: {
refute invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# invoice_items
invoice_item = InvoiceItem.last
assert_equal invoice_item.amount, training.amount_by_group(@user_without_subscription.group_id).amount
assert invoice_item.check_footprint
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user with subscription reserves a machine with success' do
login_as(@user_with_subscription, scope: :user)
plan = @user_with_subscription.subscribed_plan
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
VCR.use_cassette('reservations_create_for_machine_with_subscription_success') do
post '/api/payments/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
},
{
start_at: (availability.start_at + 1.hour).to_s(:iso8601),
end_at: (availability.start_at + 2.hours).to_s(:iso8601),
availability_id: availability.id
}
]
}
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count + 1, UsersCredit.count
# subscription assertions
assert_equal 1, @user_with_subscription.subscriptions.count
assert_not_nil @user_with_subscription.subscribed_plan
assert_equal plan.id, @user_with_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 2, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
refute invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# invoice_items assertions
invoice_items = InvoiceItem.last(2)
machine_price = machine.prices.find_by(group_id: @user_with_subscription.group_id, plan_id: plan.id).amount
assert(invoice_items.any? { |inv| inv.amount.zero? })
assert(invoice_items.any? { |inv| inv.amount == machine_price })
assert(invoice_items.all?(&:check_footprint))
# users_credits assertions
users_credit = UsersCredit.last
assert_equal @user_with_subscription, users_credit.user
assert_equal [reservation.slots.count, plan.machine_credits.find_by(creditable_id: machine.id).hours].min, users_credit.hours_used
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user with subscription reserves the FIRST training with success' do
login_as(@user_with_subscription, scope: :user)
plan = @user_with_subscription.subscribed_plan
plan.update!(is_rolling: true)
training = Training.joins(credits: :plan).where(credits: { plan: plan }).first
availability = training.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
VCR.use_cassette('reservations_create_for_training_with_subscription_success') do
post '/api/reservations',
params: {
reservation: { reservation: {
reservable_id: training.id, reservable_id: training.id,
reservable_type: training.class.name, reservable_type: training.class.name,
@ -306,349 +154,584 @@ module Reservations
} }
] ]
} }
}.to_json, headers: default_headers }
end }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
# subscription assertions
assert_equal 1, @user_with_subscription.subscriptions.count
assert_not_nil @user_with_subscription.subscribed_plan
assert_equal plan.id, @user_with_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 1, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
assert invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# invoice_items
invoice_item = InvoiceItem.last
assert_equal 0, invoice_item.amount # amount is 0 because this training is a credited training with that plan
assert invoice_item.check_footprint
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
# check that user subscription were extended
assert_equal reservation.slots.first.start_at + plan.duration, @user_with_subscription.subscription.expired_at
end end
test 'user reserves a machine and pay by wallet with success' do # general assertions
@vlonchamp = User.find_by(username: 'vlonchamp') assert_equal 201, response.status
login_as(@vlonchamp, scope: :user) assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
machine = Machine.find(6) # subscription assertions
availability = machine.availabilities.first assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
reservations_count = Reservation.count # reservation assertions
invoice_count = Invoice.count reservation = Reservation.last
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
wallet_transactions_count = WalletTransaction.count
VCR.use_cassette('reservations_create_for_machine_and_pay_wallet_success') do assert reservation.invoice
post '/api/payments/confirm_payment', assert_equal 1, reservation.invoice.invoice_items.count
params: {
payment_method_id: stripe_payment_method, # invoice assertions
cart_items: { invoice = reservation.invoice
reservation: {
user_id: @vlonchamp.id, refute invoice.stp_payment_intent_id.blank?
reservable_id: machine.id, refute invoice.total.blank?
reservable_type: machine.class.name, assert invoice.check_footprint
card_token: stripe_payment_method,
slots_attributes: [ # invoice_items
{ invoice_item = InvoiceItem.last
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601), assert_equal invoice_item.amount, training.amount_by_group(@user_without_subscription.group_id).amount
availability_id: availability.id assert invoice_item.check_footprint
}
] # invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user with subscription reserves a machine with success' do
login_as(@user_with_subscription, scope: :user)
plan = @user_with_subscription.subscribed_plan
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
VCR.use_cassette('reservations_create_for_machine_with_subscription_success') do
post '/api/payments/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
},
{
start_at: (availability.start_at + 1.hour).to_s(:iso8601),
end_at: (availability.start_at + 2.hours).to_s(:iso8601),
availability_id: availability.id
}
]
}
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count + 1, UsersCredit.count
# subscription assertions
assert_equal 1, @user_with_subscription.subscriptions.count
assert_not_nil @user_with_subscription.subscribed_plan
assert_equal plan.id, @user_with_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 2, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
refute invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# invoice_items assertions
invoice_items = InvoiceItem.last(2)
machine_price = machine.prices.find_by(group_id: @user_with_subscription.group_id, plan_id: plan.id).amount
assert(invoice_items.any? { |inv| inv.amount.zero? })
assert(invoice_items.any? { |inv| inv.amount == machine_price })
assert(invoice_items.all?(&:check_footprint))
# users_credits assertions
users_credit = UsersCredit.last
assert_equal @user_with_subscription, users_credit.user
assert_equal [reservation.slots.count, plan.machine_credits.find_by(creditable_id: machine.id).hours].min, users_credit.hours_used
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user with subscription reserves the FIRST training with success' do
login_as(@user_with_subscription, scope: :user)
plan = @user_with_subscription.subscribed_plan
plan.update!(is_rolling: true)
training = Training.joins(credits: :plan).where(credits: { plan: plan }).first
availability = training.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
VCR.use_cassette('reservations_create_for_training_with_subscription_success') do
post '/api/reservations',
params: {
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: availability.end_at.to_s(:iso8601),
availability_id: availability.id
} }
} ]
}.to_json, headers: default_headers }
end }.to_json, headers: default_headers
@vlonchamp.wallet.reload
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# subscription assertions
assert_equal 0, @vlonchamp.subscriptions.count
assert_nil @vlonchamp.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 1, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
refute invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal machine.prices.find_by(group_id: @vlonchamp.group_id, plan_id: nil).amount, invoice_item.amount
assert invoice_item.check_footprint
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal 0, @vlonchamp.wallet.amount
assert_equal 2, @vlonchamp.wallet.wallet_transactions.count
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal 10, transaction.amount
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end end
test 'user reserves a training and a subscription by wallet with success' do # general assertions
@vlonchamp = User.find_by(username: 'vlonchamp') assert_equal 201, response.status
login_as(@vlonchamp, scope: :user) assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
training = Training.first # subscription assertions
availability = training.availabilities.first assert_equal 1, @user_with_subscription.subscriptions.count
plan = Plan.find_by(group_id: @vlonchamp.group.id, type: 'Plan', base_name: 'Mensuel tarif réduit') assert_not_nil @user_with_subscription.subscribed_plan
assert_equal plan.id, @user_with_subscription.subscribed_plan.id
reservations_count = Reservation.count # reservation assertions
invoice_count = Invoice.count reservation = Reservation.last
invoice_items_count = InvoiceItem.count
wallet_transactions_count = WalletTransaction.count
VCR.use_cassette('reservations_create_for_training_and_plan_by_pay_wallet_success') do assert reservation.invoice
post '/api/payments/confirm_payment', assert_equal 1, reservation.invoice.invoice_items.count
params: {
payment_method_id: stripe_payment_method, # invoice assertions
cart_items: { invoice = reservation.invoice
reservation: {
reservable_id: training.id, assert invoice.stp_payment_intent_id.blank?
reservable_type: training.class.name, refute invoice.total.blank?
plan_id: plan.id, assert invoice.check_footprint
slots_attributes: [
{ # invoice_items
start_at: availability.start_at.to_s(:iso8601), invoice_item = InvoiceItem.last
end_at: availability.end_at.to_s(:iso8601),
availability_id: availability.id assert_equal 0, invoice_item.amount # amount is 0 because this training is a credited training with that plan
} assert invoice_item.check_footprint
]
} # invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
# check that user subscription were extended
assert_equal reservation.slots.first.start_at + plan.duration, @user_with_subscription.subscription.expired_at
end
test 'user reserves a machine and pay by wallet with success' do
@vlonchamp = User.find_by(username: 'vlonchamp')
login_as(@vlonchamp, scope: :user)
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
wallet_transactions_count = WalletTransaction.count
VCR.use_cassette('reservations_create_for_machine_and_pay_wallet_success') do
post '/api/payments/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
reservation: {
user_id: @vlonchamp.id,
reservable_id: machine.id,
reservable_type: machine.class.name,
card_token: stripe_payment_method,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
} }
}.to_json, headers: default_headers }
end }.to_json, headers: default_headers
@vlonchamp.wallet.reload
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# subscription assertions
assert_equal 1, @vlonchamp.subscriptions.count
assert_not_nil @vlonchamp.subscribed_plan
assert_equal plan.id, @vlonchamp.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 2, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
refute invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert_equal invoice.total, 2000
assert invoice.check_footprint
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal 0, @vlonchamp.wallet.amount
assert_equal 2, @vlonchamp.wallet.wallet_transactions.count
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal 10, transaction.amount
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end end
test 'user reserves a machine and a subscription using a coupon with success' do @vlonchamp.wallet.reload
login_as(@user_without_subscription, scope: :user)
machine = Machine.find(6) # general assertions
plan = Plan.where(group_id: @user_without_subscription.group_id).first assert_equal 201, response.status
availability = machine.availabilities.first assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
reservations_count = Reservation.count # subscription assertions
invoice_count = Invoice.count assert_equal 0, @vlonchamp.subscriptions.count
invoice_items_count = InvoiceItem.count assert_nil @vlonchamp.subscribed_plan
subscriptions_count = Subscription.count
users_credit_count = UsersCredit.count
VCR.use_cassette('reservations_machine_and_plan_using_coupon_success') do # reservation assertions
post '/api/payments/confirm_payment', reservation = Reservation.last
params: {
payment_method_id: stripe_payment_method, assert reservation.invoice
cart_items: { assert_equal 1, reservation.invoice.invoice_items.count
reservation: {
reservable_id: machine.id, # invoice assertions
reservable_type: machine.class.name, invoice = reservation.invoice
slots_attributes: [
{ refute invoice.stp_payment_intent_id.blank?
start_at: availability.start_at.to_s(:iso8601), refute invoice.total.blank?
end_at: (availability.start_at + 1.hour).to_s(:iso8601), assert invoice.check_footprint
availability_id: availability.id
} # invoice_items assertions
], invoice_item = InvoiceItem.last
plan_id: plan.id
}, assert_equal machine.prices.find_by(group_id: @vlonchamp.group_id, plan_id: nil).amount, invoice_item.amount
coupon_code: 'SUNNYFABLAB' assert invoice_item.check_footprint
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal 0, @vlonchamp.wallet.amount
assert_equal 2, @vlonchamp.wallet.wallet_transactions.count
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal 10, transaction.amount
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end
test 'user reserves a training and a subscription by wallet with success' do
@vlonchamp = User.find_by(username: 'vlonchamp')
login_as(@vlonchamp, scope: :user)
training = Training.first
availability = training.availabilities.first
plan = Plan.find_by(group_id: @vlonchamp.group.id, type: 'Plan', base_name: 'Mensuel tarif réduit')
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
wallet_transactions_count = WalletTransaction.count
VCR.use_cassette('reservations_create_for_training_and_plan_by_pay_wallet_success') do
post '/api/payments/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
plan_id: plan.id,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: availability.end_at.to_s(:iso8601),
availability_id: availability.id
}
]
} }
}.to_json, headers: default_headers }
end }.to_json, headers: default_headers
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal subscriptions_count + 1, Subscription.count
# subscription assertions
assert_equal 1, @user_without_subscription.subscriptions.count
assert_not_nil @user_without_subscription.subscribed_plan
assert_equal plan.id, @user_without_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 2, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
refute invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# invoice_items assertions
## reservation
reservation_item = invoice.invoice_items.where(subscription_id: nil).first
assert_not_nil reservation_item
assert_equal reservation_item.amount, machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: plan.id).amount
assert reservation_item.check_footprint
## subscription
subscription_item = invoice.invoice_items.where.not(subscription_id: nil).first
assert_not_nil subscription_item
subscription = Subscription.find(subscription_item.subscription_id)
assert_equal subscription_item.amount, plan.amount
assert_equal subscription.plan_id, plan.id
assert subscription_item.check_footprint
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
VCR.use_cassette('reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe') do
stp_intent = Stripe::PaymentIntent.retrieve(invoice.stp_payment_intent_id, api_key: Setting.get('stripe_secret_key'))
assert_equal stp_intent.amount, invoice.total
end
# notifications
assert_not_empty Notification.where(attached_object: reservation)
assert_not_empty Notification.where(attached_object: subscription)
end end
test 'user reserves a training with an expired coupon with error' do @vlonchamp.wallet.reload
login_as(@user_without_subscription, scope: :user)
training = Training.find(1) # general assertions
availability = training.availabilities.first assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
reservations_count = Reservation.count # subscription assertions
invoice_count = Invoice.count assert_equal 1, @vlonchamp.subscriptions.count
invoice_items_count = InvoiceItem.count assert_not_nil @vlonchamp.subscribed_plan
notifications_count = Notification.count assert_equal plan.id, @vlonchamp.subscribed_plan.id
VCR.use_cassette('reservations_training_with_expired_coupon_error') do # reservation assertions
post '/api/payments/confirm_payment', reservation = Reservation.last
params: {
payment_method_id: stripe_payment_method,
cart_items: {
reservation: {
user_id: @user_without_subscription.id,
reservable_id: training.id,
reservable_type: training.class.name,
card_token: stripe_payment_method,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
},
coupon_code: 'XMAS10'
}
}.to_json, headers: default_headers
end
# general assertions assert reservation.invoice
assert_equal 422, response.status assert_equal 2, reservation.invoice.invoice_items.count
assert_equal reservations_count, Reservation.count
assert_equal invoice_count, Invoice.count
assert_equal invoice_items_count, InvoiceItem.count
assert_equal notifications_count, Notification.count
# subscription assertions # invoice assertions
assert_equal 0, @user_without_subscription.subscriptions.count invoice = reservation.invoice
assert_nil @user_without_subscription.subscribed_plan
refute invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert_equal invoice.total, 2000
assert invoice.check_footprint
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal 0, @vlonchamp.wallet.amount
assert_equal 2, @vlonchamp.wallet.wallet_transactions.count
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal 10, transaction.amount
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end
test 'user reserves a machine and a subscription using a coupon with success' do
login_as(@user_without_subscription, scope: :user)
machine = Machine.find(6)
plan = Plan.where(group_id: @user_without_subscription.group_id).first
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
subscriptions_count = Subscription.count
users_credit_count = UsersCredit.count
VCR.use_cassette('reservations_machine_and_plan_using_coupon_success') do
post '/api/payments/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
],
plan_id: plan.id
},
coupon_code: 'SUNNYFABLAB'
}
}.to_json, headers: default_headers
end end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal subscriptions_count + 1, Subscription.count
# subscription assertions
assert_equal 1, @user_without_subscription.subscriptions.count
assert_not_nil @user_without_subscription.subscribed_plan
assert_equal plan.id, @user_without_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.invoice
assert_equal 2, reservation.invoice.invoice_items.count
# invoice assertions
invoice = reservation.invoice
refute invoice.stp_payment_intent_id.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# invoice_items assertions
## reservation
reservation_item = invoice.invoice_items.where(subscription_id: nil).first
assert_not_nil reservation_item
assert_equal reservation_item.amount, machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: plan.id).amount
assert reservation_item.check_footprint
## subscription
subscription_item = invoice.invoice_items.where.not(subscription_id: nil).first
assert_not_nil subscription_item
subscription = Subscription.find(subscription_item.subscription_id)
assert_equal subscription_item.amount, plan.amount
assert_equal subscription.plan_id, plan.id
assert subscription_item.check_footprint
# invoice assertions
invoice = Invoice.find_by(invoiced: reservation)
assert_invoice_pdf invoice
VCR.use_cassette('reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe') do
stp_intent = Stripe::PaymentIntent.retrieve(invoice.stp_payment_intent_id, api_key: Setting.get('stripe_secret_key'))
assert_equal stp_intent.amount, invoice.total
end
# notifications
assert_not_empty Notification.where(attached_object: reservation)
assert_not_empty Notification.where(attached_object: subscription)
end
test 'user reserves a training with an expired coupon with error' do
login_as(@user_without_subscription, scope: :user)
training = Training.find(1)
availability = training.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
notifications_count = Notification.count
VCR.use_cassette('reservations_training_with_expired_coupon_error') do
post '/api/payments/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
reservation: {
user_id: @user_without_subscription.id,
reservable_id: training.id,
reservable_type: training.class.name,
card_token: stripe_payment_method,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
},
coupon_code: 'XMAS10'
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 422, response.status
assert_equal reservations_count, Reservation.count
assert_equal invoice_count, Invoice.count
assert_equal invoice_items_count, InvoiceItem.count
assert_equal notifications_count, Notification.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
end
test 'user reserves a training and a subscription with payment schedule' do
login_as(@user_without_subscription, scope: :user)
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
subscriptions_count = Subscription.count
users_credit_count = UsersCredit.count
payment_schedule_count = PaymentSchedule.count
payment_schedule_items_count = PaymentScheduleItem.count
training = Training.find(1)
availability = training.availabilities.first
plan = Plan.find_by(group_id: @user_without_subscription.group.id, type: 'Plan', base_name: 'Abonnement mensualisable')
VCR.use_cassette('reservations_training_subscription_with_payment_schedule') do
get "/api/payments/setup_intent/#{@user_without_subscription.id}"
# Check response format & status
assert_equal 200, response.status, response.body
assert_equal Mime[:json], response.content_type
# Check the response
setup_intent = json_response(response.body)
assert_not_nil setup_intent[:client_secret]
assert_not_nil setup_intent[:id]
assert_match /^#{setup_intent[:id]}_secret_/, setup_intent[:client_secret]
# Confirm the intent
stripe_res = Stripe::SetupIntent.confirm(
setup_intent[:id],
{ payment_method: stripe_payment_method },
{ api_key: Setting.get('stripe_secret_key') }
)
# check the confirmation
assert_equal setup_intent[:id], stripe_res.id
assert_equal 'succeeded', stripe_res.status
assert_equal 'off_session', stripe_res.usage
post '/api/payments/confirm_payment_schedule',
params: {
setup_intent_id: setup_intent[:id],
cart_items: {
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
],
plan_id: plan.id,
payment_schedule: true
}
}
}.to_json, headers: default_headers
end
# Check response format & status
assert_equal 201, response.status, response.body
assert_equal Mime[:json], response.content_type
assert_equal reservations_count + 1, Reservation.count, 'missing the reservation'
assert_equal invoice_count, Invoice.count, "an invoice was generated but it shouldn't"
assert_equal invoice_items_count, InvoiceItem.count, "some invoice items were generated but they shouldn't"
assert_equal users_credit_count, UsersCredit.count, "user's credits count has changed but it shouldn't"
assert_equal subscriptions_count + 1, Subscription.count, 'missing the subscription'
assert_equal payment_schedule_count + 1, PaymentSchedule.count, 'missing the payment schedule'
assert_equal payment_schedule_items_count + 12, PaymentScheduleItem.count, 'missing some payment schedule items'
# subscription assertions
assert_equal 1, @user_without_subscription.subscriptions.count
assert_not_nil @user_without_subscription.subscribed_plan, "user's subscribed plan was not found"
assert_not_nil @user_without_subscription.subscription, "user's subscription was not found"
assert_equal plan.id, @user_without_subscription.subscribed_plan.id, "user's plan does not match"
# Check the answer
reservation = json_response(response.body)
assert_equal plan.id, reservation[:user][:subscribed_plan][:id], 'subscribed plan does not match'
end end
end end

View File

@ -1,59 +1,123 @@
# frozen_string_literal: true # frozen_string_literal: true
module Subscriptions require 'test_helper'
class CreateAsAdminTest < ActionDispatch::IntegrationTest
setup do class Subscriptions::CreateAsAdminTest < ActionDispatch::IntegrationTest
@admin = User.find_by(username: 'admin') setup do
login_as(@admin, scope: :user) @admin = User.find_by(username: 'admin')
login_as(@admin, scope: :user)
end
test 'admin successfully takes a subscription for a user' do
user = User.find_by(username: 'jdupond')
plan = Plan.find_by(group_id: user.group.id, type: 'Plan', base_name: 'Mensuel')
VCR.use_cassette('subscriptions_admin_create_success') do
post '/api/subscriptions',
params: {
subscription: {
plan_id: plan.id,
user_id: user.id
}
}.to_json, headers: default_headers
end end
test 'admin successfully takes a subscription for a user' do # Check response format & status
user = User.find_by(username: 'jdupond') assert_equal 201, response.status, response.body
plan = Plan.find_by(group_id: user.group.id, type: 'Plan', base_name: 'Mensuel') assert_equal Mime[:json], response.content_type
VCR.use_cassette('subscriptions_admin_create_success') do # Check the correct plan was subscribed
post '/api/subscriptions', subscription = json_response(response.body)
params: { assert_equal plan.id, subscription[:plan_id], 'subscribed plan does not match'
subscription: {
plan_id: plan.id, # Check that the user has only one subscription
user_id: user.id assert_equal 1, user.subscriptions.count
}
}.to_json, headers: default_headers # Check that the user has the correct subscription
end assert_not_nil user.subscription, "user's subscription was not found"
assert_not_nil user.subscription.plan, "user's subscribed plan was not found"
assert_equal plan.id, user.subscription.plan_id, "user's plan does not match"
# Check that the training credits were set correctly
assert_empty user.training_credits, 'training credits were not reset'
assert_equal user.subscription.plan.training_credit_nb, plan.training_credit_nb, 'trainings credits were not allocated'
# Check that the user benefit from prices of his plan
printer = Machine.find_by(slug: 'imprimante-3d')
assert_equal 15, (printer.prices.find_by(group_id: user.group_id, plan_id: user.subscription.plan_id).amount / 100.00), 'machine hourly price does not match'
# Check notification was sent to the user
notification = Notification.find_by(notification_type_id: NotificationType.find_by_name('notify_member_subscribed_plan'), attached_object_type: 'Subscription', attached_object_id: subscription[:id])
assert_not_nil notification, 'user notification was not created'
assert_equal user.id, notification.receiver_id, 'wrong user notified'
# Check generated invoice
invoice = Invoice.find_by(invoiced_type: 'Subscription', invoiced_id: subscription[:id])
assert_invoice_pdf invoice
assert_equal plan.amount, invoice.total, 'Invoice total price does not match the bought subscription'
end
test 'admin takes a subscription with a payment schedule' do
user = User.find_by(username: 'jdupond')
plan = Plan.find_by(group_id: user.group.id, type: 'Plan', base_name: 'Abonnement mensualisable')
invoice_count = Invoice.count
payment_schedule_count = PaymentSchedule.count
payment_schedule_items_count = PaymentScheduleItem.count
VCR.use_cassette('subscriptions_admin_create_with_payment_schedule') do
get "/api/payments/setup_intent/#{user.id}"
# Check response format & status # Check response format & status
assert_equal 201, response.status, response.body assert_equal 200, response.status, response.body
assert_equal Mime[:json], response.content_type assert_equal Mime[:json], response.content_type
# Check the correct plan was subscribed # Check the response
subscription = json_response(response.body) setup_intent = json_response(response.body)
assert_equal plan.id, subscription[:plan_id], 'subscribed plan does not match' assert_not_nil setup_intent[:client_secret]
assert_not_nil setup_intent[:id]
assert_match /^#{setup_intent[:id]}_secret_/, setup_intent[:client_secret]
# Check that the user has only one subscription # Confirm the intent
assert_equal 1, user.subscriptions.count stripe_res = Stripe::SetupIntent.confirm(
setup_intent[:id],
{ payment_method: stripe_payment_method },
{ api_key: Setting.get('stripe_secret_key') }
)
# Check that the user has the correct subscription # check the confirmation
assert_not_nil user.subscription, "user's subscription was not found" assert_equal setup_intent[:id], stripe_res.id
assert_not_nil user.subscription.plan, "user's subscribed plan was not found" assert_equal 'succeeded', stripe_res.status
assert_equal plan.id, user.subscription.plan_id, "user's plan does not match" assert_equal 'off_session', stripe_res.usage
# Check that the training credits were set correctly
assert_empty user.training_credits, 'training credits were not reset'
assert_equal user.subscription.plan.training_credit_nb, plan.training_credit_nb, 'trainings credits were not allocated'
# Check that the user benefit from prices of his plan post '/api/payments/confirm_payment_schedule',
printer = Machine.find_by(slug: 'imprimante-3d') params: {
assert_equal 15, (printer.prices.find_by(group_id: user.group_id, plan_id: user.subscription.plan_id).amount / 100.00), 'machine hourly price does not match' setup_intent_id: setup_intent[:id],
cart_items: {
# Check notification was sent to the user subscription: {
notification = Notification.find_by(notification_type_id: NotificationType.find_by_name('notify_member_subscribed_plan'), attached_object_type: 'Subscription', attached_object_id: subscription[:id]) plan_id: plan.id,
assert_not_nil notification, 'user notification was not created' payment_schedule: true,
assert_equal user.id, notification.receiver_id, 'wrong user notified' user_id: user.id,
payment_method: 'stripe'
# Check generated invoice }
invoice = Invoice.find_by(invoiced_type: 'Subscription', invoiced_id: subscription[:id]) }
assert_invoice_pdf invoice }.to_json, headers: default_headers
assert_equal plan.amount, invoice.total, 'Invoice total price does not match the bought subscription'
end end
# Check generalities
assert_equal 201, response.status, response.body
assert_equal Mime[:json], response.content_type
assert_equal invoice_count, Invoice.count, "an invoice was generated but it shouldn't"
assert_equal payment_schedule_count + 1, PaymentSchedule.count, 'missing the payment schedule'
assert_equal payment_schedule_items_count + 12, PaymentScheduleItem.count, 'missing some payment schedule items'
# Check the correct plan was subscribed
subscription = json_response(response.body)
assert_equal plan.id, subscription[:plan_id], 'subscribed plan does not match'
# Check that the user has the correct subscription
assert_not_nil user.subscription, "user's subscription was not found"
assert_not_nil user.subscription.plan, "user's subscribed plan was not found"
assert_equal plan.id, user.subscription.plan_id, "user's plan does not match"
end end
end end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'test_helper'
class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
setup do setup do
@user = User.find_by(username: 'jdupond') @user = User.find_by(username: 'jdupond')
@ -166,4 +168,63 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
assert_equal invoice.wallet_amount / 100.0, transaction.amount assert_equal invoice.wallet_amount / 100.0, transaction.amount
assert_equal invoice.wallet_transaction_id, transaction.id assert_equal invoice.wallet_transaction_id, transaction.id
end end
test 'user takes a subscription with payment schedule' do
plan = Plan.find_by(group_id: @user.group.id, type: 'Plan', base_name: 'Abonnement mensualisable')
payment_schedule_count = PaymentSchedule.count
payment_schedule_items_count = PaymentScheduleItem.count
VCR.use_cassette('subscriptions_user_create_with_payment_schedule') do
get "/api/payments/setup_intent/#{@user.id}"
# Check response format & status
assert_equal 200, response.status, response.body
assert_equal Mime[:json], response.content_type
# Check the response
setup_intent = json_response(response.body)
assert_not_nil setup_intent[:client_secret]
assert_not_nil setup_intent[:id]
assert_match /^#{setup_intent[:id]}_secret_/, setup_intent[:client_secret]
# Confirm the intent
stripe_res = Stripe::SetupIntent.confirm(
setup_intent[:id],
{ payment_method: stripe_payment_method },
{ api_key: Setting.get('stripe_secret_key') }
)
# check the confirmation
assert_equal setup_intent[:id], stripe_res.id
assert_equal 'succeeded', stripe_res.status
assert_equal 'off_session', stripe_res.usage
post '/api/payments/confirm_payment_schedule',
params: {
setup_intent_id: setup_intent[:id],
cart_items: {
subscription: {
plan_id: plan.id,
payment_schedule: true
}
}
}.to_json, headers: default_headers
end
# Check generalities
assert_equal 201, response.status, response.body
assert_equal Mime[:json], response.content_type
assert_equal payment_schedule_count + 1, PaymentSchedule.count, 'missing the payment schedule'
assert_equal payment_schedule_items_count + 12, PaymentScheduleItem.count, 'missing some payment schedule items'
# Check the correct plan was subscribed
subscription = json_response(response.body)
assert_equal plan.id, subscription[:plan_id], 'subscribed plan does not match'
# Check that the user has the correct subscription
assert_not_nil @user.subscription, "user's subscription was not found"
assert_not_nil @user.subscription.plan, "user's subscribed plan was not found"
assert_equal plan.id, @user.subscription.plan_id, "user's plan does not match"
end
end end

View File

@ -4,7 +4,7 @@ require 'coveralls'
Coveralls.wear!('rails') Coveralls.wear!('rails')
ENV['RAILS_ENV'] ||= 'test' ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../config/environment', __dir__) require_relative '../config/environment'
require 'action_dispatch' require 'action_dispatch'
require 'rails/test_help' require 'rails/test_help'
require 'vcr' require 'vcr'
@ -23,7 +23,7 @@ Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(color: true)]
class ActiveSupport::TestCase class ActiveSupport::TestCase
# Add more helper methods to be used by all tests here... # Add more helper methods to be used by all tests here...
ActiveRecord::Migration.check_pending!
fixtures :all fixtures :all
def json_response(body) def json_response(body)