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:
commit
3541688c03
@ -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:
|
||||||
|
2
Rakefile
2
Rakefile
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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 = '';
|
||||||
|
@ -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;
|
||||||
@ -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,7 +1078,7 @@ 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) {
|
||||||
@ -1095,10 +1105,9 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A
|
|||||||
$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
|
||||||
@ -1130,7 +1139,7 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A
|
|||||||
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'; }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -1164,10 +1173,9 @@ Application.Controllers.controller('NewManagerController', ['$state', '$scope',
|
|||||||
$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
|
||||||
@ -1199,6 +1207,6 @@ Application.Controllers.controller('NewManagerController', ['$state', '$scope',
|
|||||||
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'; }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
@ -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) {
|
||||||
|
// check that the online payment is enabled
|
||||||
|
if ($scope.settings.online_payment_module !== 'true') {
|
||||||
|
growl.error(_t('app.shared.cart.online_payment_disabled'));
|
||||||
|
} else {
|
||||||
$scope.toggleStripeModal(() => {
|
$scope.toggleStripeModal(() => {
|
||||||
$scope.stripe.cartItems = mkCartItems(reservation, 'stripe');
|
$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,8 +805,13 @@ 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') {
|
||||||
|
// 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();
|
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)
|
||||||
if ($scope.reservation.slots_attributes.length === 0 && selectedPlan) {
|
if ($scope.reservation.slots_attributes.length === 0 && selectedPlan) {
|
||||||
@ -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') {
|
|
||||||
growl.error(_t('app.shared.cart.online_payment_disabled'));
|
|
||||||
} else {
|
|
||||||
return payByStripe(reservation);
|
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) ||
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
end
|
||||||
|
|
||||||
price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] }
|
## Generate the subscription associated with for the current reservation
|
||||||
invoice.invoice_items.push InvoiceItem.new(
|
def generate_subscription
|
||||||
amount: price_slot[:price],
|
return unless plan_id
|
||||||
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
|
|
||||||
|
|
||||||
# check reservation amount total and strip invoice total to pay is equal
|
|
||||||
# @param stp_invoice[Stripe::Invoice]
|
|
||||||
# @param coupon_code[String]
|
|
||||||
# return Boolean
|
|
||||||
def is_equal_reservation_total_and_stp_invoice_total(stp_invoice, coupon_code = nil)
|
|
||||||
compute_amount_total_to_pay(coupon_code) == stp_invoice.total
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_payment_info(card, invoice)
|
|
||||||
card&.delete
|
|
||||||
if invoice
|
|
||||||
invoice.closed = true
|
|
||||||
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)
|
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 }
|
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(
|
subscription.init_save
|
||||||
amount: payment_details[:elements][:plan],
|
subscription
|
||||||
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
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# These actions will be realized after the reservation is initially saved (on creation)
|
||||||
|
##
|
||||||
|
def post_save
|
||||||
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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
end
|
||||||
|
payment.save
|
||||||
|
WalletService.debit_user_wallet(payment, user, reservation)
|
||||||
|
reservation.post_save
|
||||||
|
true
|
||||||
|
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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
{
|
{
|
||||||
|
@ -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?
|
||||||
|
@ -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."
|
||||||
|
@ -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."
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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'),
|
||||||
|
10
test/fixtures/history_values.yml
vendored
10
test/fixtures/history_values.yml
vendored
@ -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
|
||||||
|
6
test/fixtures/machines.yml
vendored
6
test/fixtures/machines.yml
vendored
@ -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
|
||||||
|
26
test/fixtures/plans.yml
vendored
26
test/fixtures/plans.yml
vendored
@ -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
|
||||||
|
1
test/fixtures/spaces.yml
vendored
1
test/fixtures/spaces.yml
vendored
@ -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
|
||||||
|
5
test/fixtures/trainings.yml
vendored
5
test/fixtures/trainings.yml
vendored
@ -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
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Reservations
|
require 'test_helper'
|
||||||
class CreateAsAdminTest < ActionDispatch::IntegrationTest
|
|
||||||
|
class Reservations::CreateAsAdminTest < 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
|
||||||
@ -447,5 +448,57 @@ module Reservations
|
|||||||
# notification
|
# notification
|
||||||
assert_not_empty Notification.where(attached_object: reservation)
|
assert_not_empty Notification.where(attached_object: reservation)
|
||||||
end
|
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: {
|
||||||
|
user_id: @user_without_subscription.id,
|
||||||
|
payment_method: '', # pay by check
|
||||||
|
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
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
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
|
||||||
@ -650,5 +649,89 @@ module Reservations
|
|||||||
assert_equal 0, @user_without_subscription.subscriptions.count
|
assert_equal 0, @user_without_subscription.subscriptions.count
|
||||||
assert_nil @user_without_subscription.subscribed_plan
|
assert_nil @user_without_subscription.subscribed_plan
|
||||||
end
|
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
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Subscriptions
|
require 'test_helper'
|
||||||
class CreateAsAdminTest < ActionDispatch::IntegrationTest
|
|
||||||
|
class Subscriptions::CreateAsAdminTest < ActionDispatch::IntegrationTest
|
||||||
setup do
|
setup do
|
||||||
@admin = User.find_by(username: 'admin')
|
@admin = User.find_by(username: 'admin')
|
||||||
login_as(@admin, scope: :user)
|
login_as(@admin, scope: :user)
|
||||||
@ -55,5 +56,68 @@ module Subscriptions
|
|||||||
assert_invoice_pdf invoice
|
assert_invoice_pdf invoice
|
||||||
assert_equal plan.amount, invoice.total, 'Invoice total price does not match the bought subscription'
|
assert_equal plan.amount, invoice.total, 'Invoice total price does not match the bought subscription'
|
||||||
end
|
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
|
||||||
|
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,
|
||||||
|
user_id: user.id,
|
||||||
|
payment_method: 'stripe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.to_json, headers: default_headers
|
||||||
|
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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user