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

(merge) Merge branch 'dev' into product-store

This commit is contained in:
Sylvain 2022-10-05 17:16:04 +02:00
commit a63975dd18
28 changed files with 1408 additions and 1020 deletions

View File

@ -13,7 +13,7 @@ Metrics/PerceivedComplexity:
Metrics/AbcSize:
Max: 45
Metrics/ClassLength:
Max: 200
Max: 210
Metrics/BlockLength:
Max: 30
Exclude:

View File

@ -1,11 +1,34 @@
# Changelog Fab-manager
- Script to download translations from Crowdin
- Fablab's store module
- Fix a bug: missing translations in PayZen configuration screens
- Fix a bug: wrong translation key prevents the display of the schedule deadline's payment mean
- [TODO DEPLOY] `rails db:seed`
## v5.4.21 2022 October 05
- Ability to dismiss a user to a lower privileged role
- Fix a bug: unable to generate statistics
- [TODO DEPLOY] `rails fablab:maintenance:regenerate_statistics[2022,08]`
## v5.4.20 2022 September 27
- Fix a bug: unable to show the daily view of the public agenda, if it contains trainings or events
- Fix a bug: plan's categories descriptions are not shown
- Fix a bug: groups without plans are shown but empty
- Fix a bug: unable to display the payment schedules management interface
## v5.4.19 2022 September 13
- Fix a bug: computing the wallet amount to debit ignores the applied coupon
## v5.4.18 2022 September 12
- Script to download translations from Crowdin
- Fix a bug: admin and managers can't cancel or move event reservations
- Fix a bug: phone numbers with hyphens and spaces prevent profile completion when the data is provided by an SSO
- Fix a bug: unable to complete profile from SSO when the account validation is enabled
## v5.4.17 2022 September 06
- OpenAPI spaces endpoints (index/show)

View File

@ -18,15 +18,7 @@ class API::MembersController < API::ApiController
end
def last_subscribed
@query = User.active.with_role(:member)
.includes(:statistic_profile, profile: [:user_avatar])
.where('is_allow_contact = true AND confirmed_at IS NOT NULL')
.order('created_at desc')
.limit(params[:last])
# remove unmerged profiles from list
@members = @query.to_a
@members.delete_if(&:need_completion?)
@query, @members = Members::MembersService.last_registered(params[:last])
@requested_attributes = ['profile']
render :index
@ -74,9 +66,7 @@ class API::MembersController < API::ApiController
def export_subscriptions
authorize :export
export = Export.where(category: 'users', export_type: 'subscriptions')
.where('created_at > ?', Subscription.maximum('updated_at'))
.last
export = ExportService.last_export('users/subscription')
if export.nil? || !FileTest.exist?(export.file)
@export = Export.new(category: 'users', export_type: 'subscriptions', user: current_user)
if @export.save
@ -85,7 +75,7 @@ class API::MembersController < API::ApiController
render json: @export.errors, status: :unprocessable_entity
end
else
send_file File.join(Rails.root, export.file),
send_file Rails.root.join(export.file),
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
disposition: 'attachment'
end
@ -95,9 +85,7 @@ class API::MembersController < API::ApiController
def export_reservations
authorize :export
export = Export.where(category: 'users', export_type: 'reservations')
.where('created_at > ?', Reservation.maximum('updated_at'))
.last
export = ExportService.last_export('users/reservations')
if export.nil? || !FileTest.exist?(export.file)
@export = Export.new(category: 'users', export_type: 'reservations', user: current_user)
if @export.save
@ -106,7 +94,7 @@ class API::MembersController < API::ApiController
render json: @export.errors, status: :unprocessable_entity
end
else
send_file File.join(Rails.root, export.file),
send_file Rails.root.join(export.file),
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
disposition: 'attachment'
end
@ -115,17 +103,7 @@ class API::MembersController < API::ApiController
def export_members
authorize :export
last_update = [
User.members.maximum('updated_at'),
Profile.where(user_id: User.members).maximum('updated_at'),
InvoicingProfile.where(user_id: User.members).maximum('updated_at'),
StatisticProfile.where(user_id: User.members).maximum('updated_at'),
Subscription.maximum('updated_at') || DateTime.current
].max
export = Export.where(category: 'users', export_type: 'members')
.where('created_at > ?', last_update)
.last
export = ExportService.last_export('users/members')
if export.nil? || !FileTest.exist?(export.file)
@export = Export.new(category: 'users', export_type: 'members', user: current_user)
if @export.save
@ -134,7 +112,7 @@ class API::MembersController < API::ApiController
render json: @export.errors, status: :unprocessable_entity
end
else
send_file File.join(Rails.root, export.file),
send_file Rails.root.join(export.file),
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
disposition: 'attachment'
end
@ -158,8 +136,8 @@ class API::MembersController < API::ApiController
else
render json: @member.errors, status: :unprocessable_entity
end
rescue DuplicateIndexError => error
render json: { error: t('members.please_input_the_authentication_code_sent_to_the_address', EMAIL: error.message) },
rescue DuplicateIndexError => e
render json: { error: t('members.please_input_the_authentication_code_sent_to_the_address', EMAIL: e.message) },
status: :unprocessable_entity
end
else
@ -176,7 +154,6 @@ class API::MembersController < API::ApiController
query = Members::ListService.list(query_params)
@max_members = query.except(:offset, :limit, :order).count
@members = query.to_a
end
def search
@ -196,7 +173,7 @@ class API::MembersController < API::ApiController
render json: { tours: [params[:tour]] }
else
tours = "#{@member.profile.tours} #{params[:tour]}"
@member.profile.update_attributes(tours: tours.strip)
@member.profile.update(tours: tours.strip)
render json: { tours: @member.profile.tours.split }
end
@ -205,31 +182,8 @@ class API::MembersController < API::ApiController
def update_role
authorize @member
# we do not allow dismissing a user to a lower role
if params[:role] == 'member'
render 403 and return if @member.role == 'admin' || @member.role == 'manager'
elsif params[:role] == 'manager'
render 403 and return if @member.role == 'admin'
end
# do nothing if the role does not change
render json: @member and return if params[:role] == @member.role
ex_role = @member.role.to_sym
@member.remove_role ex_role
@member.add_role params[:role]
# if the new role is 'admin', then change the group to the admins group
@member.update_attributes(group_id: Group.find_by(slug: 'admins').id) if params[:role] == 'admin'
NotificationCenter.call type: 'notify_user_role_update',
receiver: @member,
attached_object: @member
NotificationCenter.call type: 'notify_admins_role_update',
receiver: User.admins_and_managers,
attached_object: @member,
meta_data: { ex_role: ex_role }
service = Members::MembersService.new(@member)
service.update_role(params[:role], params[:group_id])
render json: @member
end
@ -265,12 +219,14 @@ class API::MembersController < API::ApiController
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy]],
{ user_avatar_attributes: %i[id attachment destroy] }],
invoicing_profile_attributes: [
:id, :organization,
address_attributes: %i[id address],
organization_attributes: [:id, :name, address_attributes: %i[id address]],
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
{
address_attributes: %i[id address],
organization_attributes: [:id, :name, { address_attributes: %i[id address] }],
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
}
],
statistic_profile_attributes: %i[id gender birthday])
@ -280,14 +236,16 @@ class API::MembersController < API::ApiController
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy]],
{ user_avatar_attributes: %i[id attachment destroy] }],
invoicing_profile_attributes: [
:id, :organization,
address_attributes: %i[id address],
organization_attributes: [:id, :name, address_attributes: %i[id address]],
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
{
address_attributes: %i[id address],
organization_attributes: [:id, :name, { address_attributes: %i[id address] }],
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
}
],
statistic_profile_attributes: [:id, :gender, :birthday, training_ids: []])
statistic_profile_attributes: [:id, :gender, :birthday, { training_ids: [] }])
end
end

View File

@ -1,7 +1,7 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { serialize } from 'object-to-formdata';
import { User, UserIndexFilter } from '../models/user';
import { User, UserIndexFilter, UserRole } from '../models/user';
export default class MemberAPI {
static async list (filters: UserIndexFilter): Promise<Array<User>> {
@ -45,6 +45,11 @@ export default class MemberAPI {
return res?.data;
}
static async updateRole (user: User, role: UserRole, groupId?: number): Promise<User> {
const res: AxiosResponse<User> = await apiClient.patch(`/api/members/${user.id}/update_role`, { role, group_id: groupId });
return res?.data;
}
static async current (): Promise<User> {
const res: AxiosResponse<User> = await apiClient.get('/api/members/current');
return res?.data;

View File

@ -208,12 +208,12 @@ const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSc
</StripeElements>
);
case 'payzen':
case null:
return (
<div>
{renderPaymentSchedulesTable()}
</div>
);
case null:
default:
console.error(`[PaymentSchedulesTable] Unimplemented gateway: ${gateway.value}`);
return <div />;

View File

@ -117,10 +117,10 @@ export const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection,
};
/**
* When called with a category ID, returns the name of the requested plan-category
* When called with a category ID, returns the requested plan-category
*/
const categoryName = (categoryId: number): string => {
return planCategories.find(c => c.id === categoryId)?.name;
const findCategory = (categoryId: number): PlanCategory => {
return planCategories.find(c => c.id === categoryId);
};
/**
@ -193,10 +193,13 @@ export const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection,
return (
<div className="list-of-categories">
{Array.from(plans).sort(compareCategories).map(([categoryId, plansByCategory]) => {
const category = findCategory(categoryId);
const categoryPlans = plansByCategory.filter(filterPlan);
const isShown = !!categoryId && categoryPlans.length > 0;
return (
<div key={categoryId} className={`plans-per-category ${categoryId ? 'with-category' : 'no-category'}`}>
{!!categoryId && categoryPlans.length > 0 && <h3 className="category-title">{ categoryName(categoryId) }</h3>}
{isShown && <h3 className="category-title">{ category.name }</h3>}
{isShown && <p className="category-description" dangerouslySetInnerHTML={{ __html: category.description }} />}
{renderPlans(categoryPlans)}
</div>
);
@ -232,7 +235,7 @@ export const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection,
{plans && Array.from(filteredPlans()).map(([groupId, plansByGroup]) => {
return (
<div key={groupId} className="plans-per-group">
<h2 className="group-title">{ groupName(groupId) }</h2>
{plansByGroup.size > 0 && <h2 className="group-title">{ groupName(groupId) }</h2>}
{plansByGroup && renderPlansByCategory(plansByGroup)}
</div>
);

View File

@ -0,0 +1,129 @@
import React, { useEffect, useState } from 'react';
import { FabModal, ModalSize } from '../base/fab-modal';
import { User, UserRole } from '../../models/user';
import { IApplication } from '../../models/application';
import { Loader } from '../base/loader';
import { react2angular } from 'react2angular';
import { useTranslation } from 'react-i18next';
import { HtmlTranslate } from '../base/html-translate';
import { useForm } from 'react-hook-form';
import MemberAPI from '../../api/member';
import { FormSelect } from '../form/form-select';
import { Group } from '../../models/group';
import GroupAPI from '../../api/group';
declare const Application: IApplication;
interface ChangeRoleModalProps {
isOpen: boolean,
toggleModal: () => void,
user: User,
onError: (message: string) => void,
onSuccess: (message: string) => void,
}
interface RoleFormData {
role: UserRole,
groupId?: number
}
/**
* Option format, expected by react-select
* @see https://github.com/JedWatson/react-select
*/
type selectRoleOption = { value: UserRole, label: string, isDisabled: boolean };
type selectGroupOption = { value: number, label: string };
/**
* This modal dialog allows to change the current role of the given user
*/
export const ChangeRoleModal: React.FC<ChangeRoleModalProps> = ({ isOpen, toggleModal, user, onSuccess, onError }) => {
const { t } = useTranslation('admin');
const { control, handleSubmit } = useForm<RoleFormData>({ defaultValues: { groupId: user.group_id } });
const [groups, setGroups] = useState<Array<Group>>([]);
const [selectedRole, setSelectedRole] = useState<UserRole>(user.role);
useEffect(() => {
GroupAPI.index({ disabled: false, admins: false }).then(setGroups).catch(onError);
}, []);
/**
* Handle the form submission: update the role on the API
*/
const onSubmit = (data: RoleFormData) => {
MemberAPI.updateRole(user, data.role, data.groupId).then(res => {
onSuccess(
t(
'app.admin.change_role_modal.role_changed',
{ OLD: t(`app.admin.change_role_modal.${user.role}`), NEW: t(`app.admin.change_role_modal.${res.role}`) }
)
);
toggleModal();
}).catch(err => onError(t('app.admin.change_role_modal.error_while_changing_role') + err));
};
/**
* Callback triggered when the user changes the selected role in the dropdown selection list
*/
const onRoleSelect = (data: UserRole) => {
setSelectedRole(data);
};
/**
* Return the various available roles for the select input
*/
const buildRolesOptions = (): Array<selectRoleOption> => {
return [
{ value: 'admin' as UserRole, label: t('app.admin.change_role_modal.admin'), isDisabled: user.role === 'admin' },
{ value: 'manager' as UserRole, label: t('app.admin.change_role_modal.manager'), isDisabled: user.role === 'manager' },
{ value: 'member' as UserRole, label: t('app.admin.change_role_modal.member'), isDisabled: user.role === 'member' }
];
};
/**
* Return the various available groups for the select input
*/
const buildGroupsOptions = (): Array<selectGroupOption> => {
return groups.map(group => {
return { value: group.id, label: group.name };
});
};
return (
<FabModal isOpen={isOpen}
toggleModal={toggleModal}
title={t('app.admin.change_role_modal.change_role')}
width={ModalSize.medium}
onConfirmSendFormId="user-role-form"
confirmButton={t('app.admin.change_role_modal.confirm')}
closeButton>
<HtmlTranslate trKey={'app.admin.change_role_modal.warning_role_change'} />
<form onSubmit={handleSubmit(onSubmit)} id="user-role-form">
<FormSelect options={buildRolesOptions()}
control={control}
id="role"
label={t('app.admin.change_role_modal.new_role')}
rules={{ required: true }}
onChange={onRoleSelect} />
{selectedRole !== 'admin' &&
<FormSelect options={buildGroupsOptions()}
control={control}
id="groupId"
label={t('app.admin.change_role_modal.new_group')}
tooltip={t('app.admin.change_role_modal.new_group_help')}
rules={{ required: true }} />}
</form>
</FabModal>
);
};
const ChangeRoleModalWrapper: React.FC<ChangeRoleModalProps> = (props) => {
return (
<Loader>
<ChangeRoleModal {...props} />
</Loader>
);
};
Application.Components.component('changeRoleModal', react2angular(ChangeRoleModalWrapper, ['isOpen', 'toggleModal', 'user', 'onError', 'onSuccess']));

View File

@ -59,7 +59,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
const { t } = useTranslation('shared');
// regular expression to validate the input fields
const phoneRegex = /^((00|\+)\d{2,3})?\d{4,14}$/;
const phoneRegex = /^((00|\+)\d{2,3})?[\d -]{4,14}$/;
const urlRegex = /^(https?:\/\/)([^.]+)\.(.{2,30})(\/.*)*\/?$/;
const { handleSubmit, register, control, formState, setValue, reset } = useForm<User>({ defaultValues: { ...user } });

View File

@ -724,6 +724,9 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
// modal dialog to take a new subscription
$scope.isOpenSubscribeModal = false;
// modal dialog to change the user's role
$scope.isOpenChangeRoleModal = false;
/**
* Open a modal dialog asking for confirmation to change the role of the given user
* @returns {*}
@ -800,6 +803,17 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
$scope.$apply();
}, 50);
};
/**
* Opens/closes the modal dialog to change the user's role
*/
$scope.toggleChangeRoleModal = () => {
setTimeout(() => {
$scope.isOpenChangeRoleModal = !$scope.isOpenChangeRoleModal;
$scope.$apply();
}, 0);
};
/**
* Callback triggered if the subscription was successfully extended
*/

View File

@ -483,6 +483,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
* @param reservation {Reservation}
*/
$scope.reservationCanModify = function (reservation) {
if (AuthService.isAuthorized(['admin', 'manager'])) return true;
const slotStart = moment(reservation.slots_reservations_attributes[0].slot_attributes.start_at);
const now = moment();
@ -498,6 +500,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
* @param reservation {Reservation}
*/
$scope.reservationCanCancel = function(reservation) {
if (AuthService.isAuthorized(['admin', 'manager'])) return true;
const slotStart = moment(reservation.slots_reservations_attributes[0].slot_attributes.start_at);
const now = moment();
return $scope.enableBookingCancel && slotStart.diff(now, "hours") >= $scope.cancelBookingDelay;

View File

@ -42,6 +42,7 @@
@import "modules/form/abstract-form-item";
@import "modules/form/form-input";
@import "modules/form/form-rich-text";
@import "modules/form/form-select";
@import "modules/form/form-switch";
@import "modules/form/form-checklist";
@import "modules/form/form-file-upload";

View File

@ -0,0 +1,9 @@
.form-select {
.rs__menu .rs__menu-list {
.rs__option {
&--is-disabled {
color: var(--gray-hard-lightest);
}
}
}
}

View File

@ -21,9 +21,10 @@
<div class="col-md-3">
<section class="heading-actions wrapper">
<div class="btn btn-lg btn-block btn-default promote-member m-t-xs" ng-click="changeUserRole()" ng-show="isAuthorized('admin')">
<div class="btn btn-lg btn-block btn-default promote-member m-t-xs" ng-click="toggleChangeRoleModal()" ng-show="isAuthorized('admin')">
<img src="/rank-icon.svg" alt="role icon" /><span class="m-l" translate>{{ 'app.admin.members_edit.change_role' }}</span>
</div>
<change-role-modal is-open="isOpenChangeRoleModal" toggle-modal="toggleChangeRoleModal" user="user" on-success="onSuccess" onError="onError" />
</section>
</div>

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
# Provides helper methods for Exports resources and properties
class ExportService
class << self
# Check if the last export of the provided type is still accurate or if it must be regenerated
def last_export(type)
case type
when 'users/members'
last_export_members
when 'users/reservations'
last_export_reservations
when 'users/subscription'
last_export_subscriptions
else
raise TypeError "unknown export type: #{type}"
end
end
private
def last_export_subscriptions
Export.where(category: 'users', export_type: 'subscriptions')
.where('created_at > ?', Subscription.maximum('updated_at'))
.last
end
def last_export_reservations
Export.where(category: 'users', export_type: 'reservations')
.where('created_at > ?', Reservation.maximum('updated_at'))
.last
end
def last_export_members
last_update = [
User.members.maximum('updated_at'),
Profile.where(user_id: User.members).maximum('updated_at'),
InvoicingProfile.where(user_id: User.members).maximum('updated_at'),
StatisticProfile.where(user_id: User.members).maximum('updated_at'),
Subscription.maximum('updated_at') || DateTime.current
].max
Export.where(category: 'users', export_type: 'members')
.where('created_at > ?', last_update)
.last
end
end
end

View File

@ -9,19 +9,19 @@ class Members::MembersService
end
def update(params)
if params[:group_id] && @member.group_id != params[:group_id].to_i && !@member.subscribed_plan.nil?
if subscriber_group_change?(params)
# here a group change is requested but unprocessable, handle the exception
@member.errors.add(:group_id, I18n.t('members.unable_to_change_the_group_while_a_subscription_is_running'))
return false
end
if params[:group_id] && params[:group_id].to_i != Group.find_by(slug: 'admins').id && @member.admin?
if admin_group_change?(params)
# an admin cannot change his group
@member.errors.add(:group_id, I18n.t('members.admins_cant_change_group'))
return false
end
group_changed = params[:group_id] && @member.group_id != params[:group_id].to_i
group_changed = user_group_change?(params)
ex_group = @member.group
user_validation_required = Setting.get('user_validation_required')
@ -80,7 +80,7 @@ class Members::MembersService
end
def validate(is_valid)
is_updated = member.update(validated_at: is_valid ? Time.now : nil)
is_updated = member.update(validated_at: is_valid ? DateTime.current : nil)
if is_updated
if is_valid
NotificationCenter.call type: 'notify_user_is_validated',
@ -107,6 +107,44 @@ class Members::MembersService
params
end
def self.last_registered(limit)
query = User.active.with_role(:member)
.includes(:statistic_profile, profile: [:user_avatar])
.where('is_allow_contact = true AND confirmed_at IS NOT NULL')
.order('created_at desc')
.limit(limit)
# remove unmerged profiles from list
members = query.to_a
members.delete_if(&:need_completion?)
[query, members]
end
def update_role(new_role, new_group_id = Group.first.id)
# do nothing if the role does not change
return if new_role == @member.role
# update role
ex_role = @member.role.to_sym
@member.remove_role ex_role
@member.add_role new_role
# if the new role is 'admin', then change the group to the admins group, otherwise to change to the provided group
group_id = new_role == 'admin' ? Group.find_by(slug: 'admins').id : new_group_id
@member.update(group_id: group_id)
# notify
NotificationCenter.call type: 'notify_user_role_update',
receiver: @member,
attached_object: @member
NotificationCenter.call type: 'notify_admins_role_update',
receiver: User.admins_and_managers,
attached_object: @member,
meta_data: { ex_role: ex_role }
end
private
def notify_user_profile_complete(previous_state)
@ -133,4 +171,16 @@ class Members::MembersService
params[:password]
end
end
def subscriber_group_change?(params)
params[:group_id] && @member.group_id != params[:group_id].to_i && !@member.subscribed_plan.nil?
end
def admin_group_change?(params)
params[:group_id] && params[:group_id].to_i != Group.find_by(slug: 'admins').id && @member.admin?
end
def user_group_change?(params)
@member.group_id && params[:group_id] && @member.group_id != params[:group_id].to_i
end
end

View File

@ -12,5 +12,15 @@ class Statistics::BuilderService
Statistics::Builders::MembersBuilderService.build(options)
Statistics::Builders::ProjectsBuilderService.build(options)
end
private
def default_options
yesterday = 1.day.ago
{
start_date: yesterday.beginning_of_day,
end_date: yesterday.end_of_day
}
end
end
end

View File

@ -24,7 +24,7 @@ class WalletService
NotificationCenter.call type: 'notify_admin_user_wallet_is_credited',
receiver: User.admins_and_managers,
attached_object: transaction
return transaction
transaction
end
end
raise ActiveRecord::Rollback
@ -43,7 +43,7 @@ class WalletService
amount: amount
)
return transaction if transaction.save
transaction if transaction.save
end
raise ActiveRecord::Rollback
end
@ -77,7 +77,6 @@ class WalletService
# Compute the amount decreased from the user's wallet, if applicable
# @param payment {Invoice|PaymentSchedule|Order}
# @param user {User} the customer
# @param coupon {Coupon|String} Coupon object or code
##
def self.wallet_amount_debit(payment, user)
total = if payment.is_a? PaymentSchedule

View File

@ -49,10 +49,10 @@ json.array!(@availabilities) do |availability|
json.borderColor space_slot_border_color(availability)
when 'training'
json.training_id availability.availability.trainings.first.id
json.borderColor trainings_events_border_color(availability)
json.borderColor trainings_events_border_color(availability.availability)
when 'event'
json.event_id availability.availability.event.id
json.borderColor trainings_events_border_color(availability)
json.borderColor trainings_events_border_color(availability.availability)
else
json.title 'Unknown slot'
end

View File

@ -985,15 +985,20 @@ en:
to_complete: "To complete"
refuse_documents: "Refusing the documents"
refuse_documents_info: "After verification, you may notify the member that the evidence submitted is not acceptable. You can specify the reasons for your refusal and indicate the actions to be taken. The member will be notified by e-mail."
#edit a member
members_edit:
change_role_modal:
change_role: "Change role"
warning_role_change: "<p><strong>Warning:</strong> changing the role of a user is not a harmless operation. Is not currently possible to dismiss a user to a lower privileged role.</p><ul><li><strong>Members</strong> can only book reservations for themselves, paying by card or wallet.</li><li><strong>Managers</strong> can book reservations for themselves, paying by card or wallet, and for other members and managers, by collecting payments at the checkout.</li><li><strong>Administrators</strong> can only book reservations for members and managers, by collecting payments at the checkout. Moreover, they can change every settings of the application.</li></ul>"
warning_role_change: "<p><strong>Warning:</strong> changing the role of a user is not a harmless operation.</p><ul><li><strong>Members</strong> can only book reservations for themselves, paying by card or wallet.</li><li><strong>Managers</strong> can book reservations for themselves, paying by card or wallet, and for other members and managers, by collecting payments at the checkout.</li><li><strong>Administrators</strong> can only book reservations for members and managers, by collecting payments at the checkout. Moreover, they can change every settings of the application.</li></ul>"
new_role: "New role"
admin: "Administrator"
manager: "Manager"
member: "Member"
new_group: "New group"
new_group_help: "Members and managers must be placed in a group."
confirm: "Change role"
role_changed: "Role successfully changed from {OLD} to {NEW}."
error_while_changing_role: "An error occurred while changing the role. Please try again later."
#edit a member
members_edit:
subscription: "Subscription"
duration: "Duration:"
expires_at: "Expires at:"

View File

@ -14,7 +14,7 @@
### User's manual
The following guide describes what you can do and how to use Fab-manager.
- [Français](fr/guide_utilisation_fab_manager_v5.0.pdf)
- [Français](http://guide-fr.fab.mn/)
### System administrator
The following guides are designed for the people that perform software maintenance.
@ -40,7 +40,7 @@ The following guides are designed for the people that perform software maintenan
- [ElasticSearch](elastic_upgrade.md)
### Translator's documentation
If you intend to translate Fab-manager to a new, or an already supported language, you'll find here the information you need.
If you intend to translate Fab-manager to a new, or an already supported language, you'll find here the information you need.
- [Guide for translators](translation_readme.md)
### Developer's documentation

View File

@ -1,6 +1,6 @@
{
"name": "fab-manager",
"version": "5.4.17",
"version": "5.4.21",
"description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.",
"keywords": [
"fablab",

View File

@ -1,927 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
module Reservations; end
class Reservations::CreateTest < ActionDispatch::IntegrationTest
setup do
@user_without_subscription = User.members.without_subscription.first
@user_with_subscription = User.members.with_subscription.second
end
test 'user without subscription reserves a machine with success' do
login_as(@user_without_subscription, scope: :user)
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
subscriptions_count = Subscription.count
VCR.use_cassette('reservations_create_for_machine_without_subscription_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal subscriptions_count, Subscription.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 1, reservation.original_invoice.invoice_items.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: nil).amount, invoice_item.amount
assert invoice_item.check_footprint
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
refute invoice.payment_gateway_object.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user without subscription reserves a machine with error' do
login_as(@user_without_subscription, scope: :user)
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
notifications_count = Notification.count
VCR.use_cassette('reservations_create_for_machine_without_subscription_error') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method(error: :card_declined),
cart_items: {
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
# Check response format & status
assert_equal 200, response.status, "API does not return the expected status. #{response.body}"
assert_equal Mime[:json], response.content_type
# Check the error was handled
assert_match /Your card was declined/, response.body
# Check the subscription wasn't taken
assert_equal reservations_count, Reservation.count
assert_equal invoice_count, Invoice.count
assert_equal invoice_items_count, InvoiceItem.count
assert_equal notifications_count, Notification.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
end
test 'user without subscription reserves a training with success' do
login_as(@user_without_subscription, scope: :user)
training = Training.first
availability = training.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
VCR.use_cassette('reservations_create_for_training_without_subscription_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 1, reservation.original_invoice.invoice_items.count
# invoice_items
invoice_item = InvoiceItem.last
assert_equal invoice_item.amount, training.amount_by_group(@user_without_subscription.group_id).amount
assert invoice_item.check_footprint
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
refute invoice.payment_gateway_object.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user with subscription reserves a machine with success' do
login_as(@user_with_subscription, scope: :user)
plan = @user_with_subscription.subscribed_plan
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
VCR.use_cassette('reservations_create_for_machine_with_subscription_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
},
{
slot_id: availability.slots.last.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count + 1, UsersCredit.count
# subscription assertions
assert_equal 1, @user_with_subscription.subscriptions.count
assert_not_nil @user_with_subscription.subscribed_plan
assert_equal plan.id, @user_with_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 2, reservation.original_invoice.invoice_items.count
# invoice_items assertions
invoice_items = InvoiceItem.last(2)
machine_price = machine.prices.find_by(group_id: @user_with_subscription.group_id, plan_id: plan.id).amount
assert(invoice_items.any? { |inv| inv.amount.zero? })
assert(invoice_items.any? { |inv| inv.amount == machine_price })
assert(invoice_items.all?(&:check_footprint))
# users_credits assertions
users_credit = UsersCredit.last
assert_equal @user_with_subscription, users_credit.user
assert_equal [reservation.slots.count, plan.machine_credits.find_by(creditable_id: machine.id).hours].min, users_credit.hours_used
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
refute invoice.payment_gateway_object.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user with subscription reserves the FIRST training with success' do
login_as(@user_with_subscription, scope: :user)
plan = @user_with_subscription.subscribed_plan
plan.update!(is_rolling: true)
training = Training.joins(credits: :plan).where(credits: { plan: plan }).first
availability = training.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
VCR.use_cassette('reservations_create_for_training_with_subscription_success') do
post '/api/local_payment/confirm_payment',
params: {
items: [
{
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
# subscription assertions
assert_equal 1, @user_with_subscription.subscriptions.count
assert_not_nil @user_with_subscription.subscribed_plan
assert_equal plan.id, @user_with_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 1, reservation.original_invoice.invoice_items.count
# invoice_items
invoice_item = InvoiceItem.last
assert_equal 0, invoice_item.amount # amount is 0 because this training is a credited training with that plan
assert invoice_item.check_footprint
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert invoice.payment_gateway_object.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
# check that user subscription were extended
assert_equal reservation.slots.first.start_at + plan.duration, @user_with_subscription.subscription.expired_at
end
test 'user reserves a machine and pay by wallet with success' do
@vlonchamp = User.find_by(username: 'vlonchamp')
login_as(@vlonchamp, scope: :user)
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
wallet_transactions_count = WalletTransaction.count
VCR.use_cassette('reservations_create_for_machine_and_pay_wallet_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
customer_id: @vlonchamp.id,
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
@vlonchamp.wallet.reload
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# subscription assertions
assert_equal 0, @vlonchamp.subscriptions.count
assert_nil @vlonchamp.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 1, reservation.original_invoice.invoice_items.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal machine.prices.find_by(group_id: @vlonchamp.group_id, plan_id: nil).amount, invoice_item.amount
assert invoice_item.check_footprint
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
refute invoice.payment_gateway_object.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal 0, @vlonchamp.wallet.amount
assert_equal 2, @vlonchamp.wallet.wallet_transactions.count
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal 10, transaction.amount
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end
test 'user reserves a training and a subscription by wallet with success' do
@vlonchamp = User.find_by(username: 'vlonchamp')
login_as(@vlonchamp, scope: :user)
training = Training.first
availability = training.availabilities.first
plan = Plan.find_by(group_id: @vlonchamp.group.id, type: 'Plan', base_name: 'Mensuel tarif réduit')
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
wallet_transactions_count = WalletTransaction.count
VCR.use_cassette('reservations_create_for_training_and_plan_by_pay_wallet_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
},
{
subscription: {
plan_id: plan.id
}
}
]
}
}.to_json, headers: default_headers
end
@vlonchamp.wallet.reload
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# subscription assertions
assert_equal 1, @vlonchamp.subscriptions.count
assert_not_nil @vlonchamp.subscribed_plan
assert_equal plan.id, @vlonchamp.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 2, reservation.original_invoice.invoice_items.count
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
refute invoice.payment_gateway_object.blank?
refute invoice.total.blank?
assert_equal invoice.total, 2000
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal 0, @vlonchamp.wallet.amount
assert_equal 2, @vlonchamp.wallet.wallet_transactions.count
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal 10, transaction.amount
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end
test 'user reserves a machine and a subscription using a coupon with success' do
login_as(@user_without_subscription, scope: :user)
machine = Machine.find(6)
plan = Plan.find(4)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
subscriptions_count = Subscription.count
users_credit_count = UsersCredit.count
VCR.use_cassette('reservations_machine_and_plan_using_coupon_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
},
{
subscription: {
plan_id: plan.id
}
}
],
coupon_code: 'SUNNYFABLAB'
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal subscriptions_count + 1, Subscription.count
# subscription assertions
assert_equal 1, @user_without_subscription.subscriptions.count
assert_not_nil @user_without_subscription.subscribed_plan
assert_equal plan.id, @user_without_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 2, reservation.original_invoice.invoice_items.count
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
refute invoice.payment_gateway_object.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# invoice_items assertions
## reservation
reservation_item = invoice.invoice_items.find_by(object: reservation)
assert_not_nil reservation_item
assert_equal reservation_item.amount, machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: plan.id).amount
assert reservation_item.check_footprint
## subscription
subscription_item = invoice.invoice_items.find_by(object_type: Subscription.name)
assert_not_nil subscription_item
subscription = subscription_item.object
assert_equal subscription_item.amount, plan.amount
assert_equal subscription.plan_id, plan.id
assert subscription_item.check_footprint
VCR.use_cassette('reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe') do
stp_intent = invoice.payment_gateway_object.gateway_object.retrieve
assert_equal stp_intent.amount, invoice.total
end
# notifications
assert_not_empty Notification.where(attached_object: reservation)
assert_not_empty Notification.where(attached_object: subscription)
end
test 'user reserves a training with an expired coupon with error' do
login_as(@user_without_subscription, scope: :user)
training = Training.find(1)
availability = training.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
notifications_count = Notification.count
VCR.use_cassette('reservations_training_with_expired_coupon_error') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
customer_id: @user_without_subscription.id,
items: [
{
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
card_token: stripe_payment_method,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
],
coupon_code: 'XMAS10'
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 422, response.status
assert_equal reservations_count, Reservation.count
assert_equal invoice_count, Invoice.count
assert_equal invoice_items_count, InvoiceItem.count
assert_equal notifications_count, Notification.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
end
test 'user reserves a training and a subscription with payment schedule' do
login_as(@user_without_subscription, scope: :user)
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
subscriptions_count = Subscription.count
users_credit_count = UsersCredit.count
payment_schedule_count = PaymentSchedule.count
payment_schedule_items_count = PaymentScheduleItem.count
training = Training.find(1)
availability = training.availabilities.first
plan = Plan.find_by(group_id: @user_without_subscription.group.id, type: 'Plan', base_name: 'Abonnement mensualisable')
VCR.use_cassette('reservations_training_subscription_with_payment_schedule') do
post '/api/stripe/setup_subscription',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
},
{
subscription: {
plan_id: plan.id
}
}
],
payment_schedule: true,
payment_method: 'cart'
}
}.to_json, headers: default_headers
# Check response format & status
assert_equal 201, response.status, response.body
assert_equal Mime[:json], response.content_type
# Check the response
sub = json_response(response.body)
assert_not_nil sub[:id]
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'
# get the objects
reservation = Reservation.last
payment_schedule = PaymentSchedule.last
# 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"
# reservation assertions
assert reservation.original_payment_schedule
assert_equal payment_schedule.main_object.object, reservation
# Check the answer
result = json_response(response.body)
assert_equal payment_schedule.id, result[:id], 'payment schedule id does not match'
subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }.object
assert_equal plan.id, subscription.plan_id, 'subscribed plan does not match'
end
test 'user reserves a machine and renew a subscription with payment schedule and coupon and wallet' do
user = User.find_by(username: 'lseguin')
login_as(user, scope: :user)
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
subscriptions_count = Subscription.count
user_subscriptions_count = user.subscriptions.count
payment_schedule_count = PaymentSchedule.count
payment_schedule_items_count = PaymentScheduleItem.count
wallet_transactions_count = WalletTransaction.count
machine = Machine.find(1)
availability = machine.availabilities.last
plan = Plan.find_by(group_id: user.group.id, type: 'Plan', base_name: 'Abonnement mensualisable')
VCR.use_cassette('reservations_machine_subscription_with_payment_schedule_coupon_wallet') do
post '/api/stripe/setup_subscription',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
},
{
subscription: {
plan_id: plan.id
}
}
],
payment_schedule: true,
payment_method: 'card',
coupon_code: 'GIME3EUR'
}
}.to_json, headers: default_headers
# Check response format & status
assert_equal 201, response.status, response.body
assert_equal Mime[:json], response.content_type
# Check the response
res = json_response(response.body)
assert_not_nil res[:id]
end
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 0, UsersCredit.count, "user's credits were not reset"
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'
assert_equal wallet_transactions_count + 1, WalletTransaction.count, 'missing the wallet transaction'
# get the objects
reservation = Reservation.last
subscription = Subscription.last
payment_schedule = PaymentSchedule.last
# subscription assertions
assert_equal user_subscriptions_count + 1, user.subscriptions.count
assert_equal user, subscription.user
assert_not_nil user.subscribed_plan, "user's subscribed plan was not found"
assert_not_nil user.subscription, "user's subscription was not found"
assert_equal plan.id, user.subscribed_plan.id, "user's plan does not match"
# reservation assertions
assert reservation.original_payment_schedule
assert_equal payment_schedule.main_object.object, reservation
# payment schedule assertions
assert_not_nil payment_schedule.reference
assert_equal 'card', payment_schedule.payment_method
assert_equal 2, payment_schedule.payment_gateway_objects.count
assert_not_nil payment_schedule.gateway_payment_mean
assert_not_nil payment_schedule.wallet_transaction
assert_equal payment_schedule.ordered_items.first.amount, payment_schedule.wallet_amount
assert_equal Coupon.find_by(code: 'GIME3EUR').id, payment_schedule.coupon_id
assert_equal 'test', payment_schedule.environment
assert payment_schedule.check_footprint
assert_equal user.invoicing_profile.id, payment_schedule.invoicing_profile_id
assert_equal payment_schedule.invoicing_profile_id, payment_schedule.operator_profile_id
# Check the answer
result = json_response(response.body)
assert_equal payment_schedule.id, result[:id], 'payment schedule id does not match'
subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }.object
assert_equal plan.id, subscription.plan_id, 'subscribed plan does not match'
end
test 'user reserves a space with success' do
login_as(@user_without_subscription, scope: :user)
space = Space.first
availability = space.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
subscriptions_count = Subscription.count
VCR.use_cassette('reservations_create_for_space_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: space.id,
reservable_type: space.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal subscriptions_count, Subscription.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 1, reservation.original_invoice.invoice_items.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal space.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: nil).amount, invoice_item.amount
assert invoice_item.check_footprint
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
refute invoice.payment_gateway_object.blank?
refute invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
end

View File

@ -0,0 +1,277 @@
# frozen_string_literal: true
require 'test_helper'
module Reservations; end
class Reservations::PayWithWalletTest < ActionDispatch::IntegrationTest
setup do
@vlonchamp = User.find_by(username: 'vlonchamp')
end
test 'user reserves a machine and pay by wallet with success' do
login_as(@vlonchamp, scope: :user)
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
wallet_transactions_count = WalletTransaction.count
VCR.use_cassette('reservations_create_for_machine_and_pay_wallet_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
customer_id: @vlonchamp.id,
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
@vlonchamp.wallet.reload
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# subscription assertions
assert_equal 0, @vlonchamp.subscriptions.count
assert_nil @vlonchamp.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 1, reservation.original_invoice.invoice_items.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal machine.prices.find_by(group_id: @vlonchamp.group_id, plan_id: nil).amount, invoice_item.amount
assert invoice_item.check_footprint
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert_not invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal 0, @vlonchamp.wallet.amount
assert_equal 2, @vlonchamp.wallet.wallet_transactions.count
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal 10, transaction.amount
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end
test 'user reserves a training and a subscription by wallet with success' do
login_as(@vlonchamp, scope: :user)
training = Training.first
availability = training.availabilities.first
plan = Plan.find_by(group_id: @vlonchamp.group.id, type: 'Plan', base_name: 'Mensuel tarif réduit')
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
wallet_transactions_count = WalletTransaction.count
VCR.use_cassette('reservations_create_for_training_and_plan_by_pay_wallet_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
},
{
subscription: {
plan_id: plan.id
}
}
]
}
}.to_json, headers: default_headers
end
@vlonchamp.wallet.reload
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# subscription assertions
assert_equal 1, @vlonchamp.subscriptions.count
assert_not_nil @vlonchamp.subscribed_plan
assert_equal plan.id, @vlonchamp.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 2, reservation.original_invoice.invoice_items.count
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert_not invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert_equal invoice.total, 2000
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal 0, @vlonchamp.wallet.amount
assert_equal 2, @vlonchamp.wallet.wallet_transactions.count
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal 10, transaction.amount
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end
test 'user reserves a machine and renew a subscription with payment schedule and coupon and wallet' do
user = User.find_by(username: 'lseguin')
login_as(user, scope: :user)
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
subscriptions_count = Subscription.count
user_subscriptions_count = user.subscriptions.count
payment_schedule_count = PaymentSchedule.count
payment_schedule_items_count = PaymentScheduleItem.count
wallet_transactions_count = WalletTransaction.count
machine = Machine.find(1)
availability = machine.availabilities.last
plan = Plan.find_by(group_id: user.group.id, type: 'Plan', base_name: 'Abonnement mensualisable')
VCR.use_cassette('reservations_machine_subscription_with_payment_schedule_coupon_wallet') do
post '/api/stripe/setup_subscription',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
},
{
subscription: {
plan_id: plan.id
}
}
],
payment_schedule: true,
payment_method: 'card',
coupon_code: 'GIME3EUR'
}
}.to_json, headers: default_headers
# Check response format & status
assert_equal 201, response.status, response.body
assert_equal Mime[:json], response.content_type
# Check the response
res = json_response(response.body)
assert_not_nil res[:id]
end
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 0, UsersCredit.count, "user's credits were not reset"
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'
assert_equal wallet_transactions_count + 1, WalletTransaction.count, 'missing the wallet transaction'
# get the objects
reservation = Reservation.last
subscription = Subscription.last
payment_schedule = PaymentSchedule.last
# subscription assertions
assert_equal user_subscriptions_count + 1, user.subscriptions.count
assert_equal user, subscription.user
assert_not_nil user.subscribed_plan, "user's subscribed plan was not found"
assert_not_nil user.subscription, "user's subscription was not found"
assert_equal plan.id, user.subscribed_plan.id, "user's plan does not match"
# reservation assertions
assert reservation.original_payment_schedule
assert_equal payment_schedule.main_object.object, reservation
# payment schedule assertions
assert_not_nil payment_schedule.reference
assert_equal 'card', payment_schedule.payment_method
assert_equal 2, payment_schedule.payment_gateway_objects.count
assert_not_nil payment_schedule.gateway_payment_mean
assert_not_nil payment_schedule.wallet_transaction
assert_equal CouponService.new.apply(payment_schedule.ordered_items.first.amount, payment_schedule.coupon, user.id),
payment_schedule.wallet_amount
assert_equal Coupon.find_by(code: 'GIME3EUR').id, payment_schedule.coupon_id
assert_equal 'test', payment_schedule.environment
assert payment_schedule.check_footprint
assert_equal user.invoicing_profile.id, payment_schedule.invoicing_profile_id
assert_equal payment_schedule.invoicing_profile_id, payment_schedule.operator_profile_id
# Check the answer
result = json_response(response.body)
assert_equal payment_schedule.id, result[:id], 'payment schedule id does not match'
subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }.object
assert_equal plan.id, subscription.plan_id, 'subscribed plan does not match'
end
end

View File

@ -0,0 +1,232 @@
# frozen_string_literal: true
require 'test_helper'
module Reservations; end
class Reservations::ReserveMachineTest < ActionDispatch::IntegrationTest
setup do
@user_without_subscription = User.members.without_subscription.first
end
test 'user without subscription reserves a machine with success' do
login_as(@user_without_subscription, scope: :user)
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
subscriptions_count = Subscription.count
VCR.use_cassette('reservations_create_for_machine_without_subscription_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal subscriptions_count, Subscription.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 1, reservation.original_invoice.invoice_items.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: nil).amount, invoice_item.amount
assert invoice_item.check_footprint
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert_not invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user without subscription reserves a machine with error' do
login_as(@user_without_subscription, scope: :user)
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
notifications_count = Notification.count
VCR.use_cassette('reservations_create_for_machine_without_subscription_error') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method(error: :card_declined),
cart_items: {
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
# Check response format & status
assert_equal 200, response.status, "API does not return the expected status. #{response.body}"
assert_equal Mime[:json], response.content_type
# Check the error was handled
assert_match(/Your card was declined/, response.body)
# Check the subscription wasn't taken
assert_equal reservations_count, Reservation.count
assert_equal invoice_count, Invoice.count
assert_equal invoice_items_count, InvoiceItem.count
assert_equal notifications_count, Notification.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
end
test 'user reserves a machine and a subscription using a coupon with success' do
login_as(@user_without_subscription, scope: :user)
machine = Machine.find(6)
plan = Plan.find(4)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
subscriptions_count = Subscription.count
users_credit_count = UsersCredit.count
VCR.use_cassette('reservations_machine_and_plan_using_coupon_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
},
{
subscription: {
plan_id: plan.id
}
}
],
coupon_code: 'SUNNYFABLAB'
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal subscriptions_count + 1, Subscription.count
# subscription assertions
assert_equal 1, @user_without_subscription.subscriptions.count
assert_not_nil @user_without_subscription.subscribed_plan
assert_equal plan.id, @user_without_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 2, reservation.original_invoice.invoice_items.count
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert_not invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# invoice_items assertions
## reservation
reservation_item = invoice.invoice_items.find_by(object: reservation)
assert_not_nil reservation_item
assert_equal reservation_item.amount, machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: plan.id).amount
assert reservation_item.check_footprint
## subscription
subscription_item = invoice.invoice_items.find_by(object_type: Subscription.name)
assert_not_nil subscription_item
subscription = subscription_item.object
assert_equal subscription_item.amount, plan.amount
assert_equal subscription.plan_id, plan.id
assert subscription_item.check_footprint
VCR.use_cassette('reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe') do
stp_intent = invoice.payment_gateway_object.gateway_object.retrieve
assert_equal stp_intent.amount, invoice.total
end
# notifications
assert_not_empty Notification.where(attached_object: reservation)
assert_not_empty Notification.where(attached_object: subscription)
end
end

View File

@ -0,0 +1,83 @@
# frozen_string_literal: true
require 'test_helper'
module Reservations; end
class Reservations::ReserveSpaceTest < ActionDispatch::IntegrationTest
setup do
@user_without_subscription = User.members.without_subscription.first
end
test 'user reserves a space with success' do
login_as(@user_without_subscription, scope: :user)
space = Space.first
availability = space.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
subscriptions_count = Subscription.count
VCR.use_cassette('reservations_create_for_space_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: space.id,
reservable_type: space.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal subscriptions_count, Subscription.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 1, reservation.original_invoice.invoice_items.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal space.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: nil).amount, invoice_item.amount
assert invoice_item.check_footprint
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert_not invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
end

View File

@ -0,0 +1,211 @@
# frozen_string_literal: true
require 'test_helper'
module Reservations; end
class Reservations::ReserveTrainingTest < ActionDispatch::IntegrationTest
setup do
@user_without_subscription = User.members.without_subscription.first
end
test 'user without subscription reserves a training with success' do
login_as(@user_without_subscription, scope: :user)
training = Training.first
availability = training.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
VCR.use_cassette('reservations_create_for_training_without_subscription_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 1, reservation.original_invoice.invoice_items.count
# invoice_items
invoice_item = InvoiceItem.last
assert_equal invoice_item.amount, training.amount_by_group(@user_without_subscription.group_id).amount
assert invoice_item.check_footprint
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert_not invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user reserves a training with an expired coupon with error' do
login_as(@user_without_subscription, scope: :user)
training = Training.find(1)
availability = training.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
notifications_count = Notification.count
VCR.use_cassette('reservations_training_with_expired_coupon_error') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
customer_id: @user_without_subscription.id,
items: [
{
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
card_token: stripe_payment_method,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
],
coupon_code: 'XMAS10'
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 422, response.status
assert_equal reservations_count, Reservation.count
assert_equal invoice_count, Invoice.count
assert_equal invoice_items_count, InvoiceItem.count
assert_equal notifications_count, Notification.count
# subscription assertions
assert_equal 0, @user_without_subscription.subscriptions.count
assert_nil @user_without_subscription.subscribed_plan
end
test 'user reserves a training and a subscription with payment schedule' do
login_as(@user_without_subscription, scope: :user)
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
subscriptions_count = Subscription.count
users_credit_count = UsersCredit.count
payment_schedule_count = PaymentSchedule.count
payment_schedule_items_count = PaymentScheduleItem.count
training = Training.find(1)
availability = training.availabilities.first
plan = Plan.find_by(group_id: @user_without_subscription.group.id, type: 'Plan', base_name: 'Abonnement mensualisable')
VCR.use_cassette('reservations_training_subscription_with_payment_schedule') do
post '/api/stripe/setup_subscription',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
},
{
subscription: {
plan_id: plan.id
}
}
],
payment_schedule: true,
payment_method: 'cart'
}
}.to_json, headers: default_headers
# Check response format & status
assert_equal 201, response.status, response.body
assert_equal Mime[:json], response.content_type
# Check the response
sub = json_response(response.body)
assert_not_nil sub[:id]
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'
# get the objects
reservation = Reservation.last
payment_schedule = PaymentSchedule.last
# 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"
# reservation assertions
assert reservation.original_payment_schedule
assert_equal payment_schedule.main_object.object, reservation
# Check the answer
result = json_response(response.body)
assert_equal payment_schedule.id, result[:id], 'payment schedule id does not match'
subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }.object
assert_equal plan.id, subscription.plan_id, 'subscribed plan does not match'
end
end

View File

@ -0,0 +1,249 @@
# frozen_string_literal: true
require 'test_helper'
module Reservations; end
class Reservations::WithSubscriptionTest < ActionDispatch::IntegrationTest
setup do
@user_with_subscription = User.members.with_subscription.second
end
test 'user with subscription reserves a machine with success' do
login_as(@user_with_subscription, scope: :user)
plan = @user_with_subscription.subscribed_plan
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
VCR.use_cassette('reservations_create_for_machine_with_subscription_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
},
{
slot_id: availability.slots.last.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count + 1, UsersCredit.count
# subscription assertions
assert_equal 1, @user_with_subscription.subscriptions.count
assert_not_nil @user_with_subscription.subscribed_plan
assert_equal plan.id, @user_with_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 2, reservation.original_invoice.invoice_items.count
# invoice_items assertions
invoice_items = InvoiceItem.last(2)
machine_price = machine.prices.find_by(group_id: @user_with_subscription.group_id, plan_id: plan.id).amount
assert(invoice_items.any? { |inv| inv.amount.zero? })
assert(invoice_items.any? { |inv| inv.amount == machine_price })
assert(invoice_items.all?(&:check_footprint))
# users_credits assertions
users_credit = UsersCredit.last
assert_equal @user_with_subscription, users_credit.user
assert_equal [reservation.slots.count, plan.machine_credits.find_by(creditable_id: machine.id).hours].min, users_credit.hours_used
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert_not invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
end
test 'user with subscription reserves the FIRST training with success' do
login_as(@user_with_subscription, scope: :user)
plan = @user_with_subscription.subscribed_plan
plan.update!(is_rolling: true)
training = Training.joins(credits: :plan).where(credits: { plan: plan }).first
availability = training.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
VCR.use_cassette('reservations_create_for_training_with_subscription_success') do
post '/api/local_payment/confirm_payment',
params: {
items: [
{
reservation: {
reservable_id: training.id,
reservable_type: training.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}.to_json, headers: default_headers
end
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
# subscription assertions
assert_equal 1, @user_with_subscription.subscriptions.count
assert_not_nil @user_with_subscription.subscribed_plan
assert_equal plan.id, @user_with_subscription.subscribed_plan.id
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 1, reservation.original_invoice.invoice_items.count
# invoice_items
invoice_item = InvoiceItem.last
assert_equal 0, invoice_item.amount # amount is 0 because this training is a credited training with that plan
assert invoice_item.check_footprint
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
# check that user subscription were extended
assert_equal reservation.slots.first.start_at + plan.duration, @user_with_subscription.subscription.expired_at
end
test 'user reserves a machine and pay by wallet with success' do
@vlonchamp = User.find_by(username: 'vlonchamp')
login_as(@vlonchamp, scope: :user)
machine = Machine.find(6)
availability = machine.availabilities.first
reservations_count = Reservation.count
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
wallet_transactions_count = WalletTransaction.count
VCR.use_cassette('reservations_create_for_machine_and_pay_wallet_success') do
post '/api/stripe/confirm_payment',
params: {
payment_method_id: stripe_payment_method,
cart_items: {
customer_id: @vlonchamp.id,
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_reservations_attributes: [
{
slot_id: availability.slots.first.id
}
]
}
}
]
}
}.to_json, headers: default_headers
end
@vlonchamp.wallet.reload
# general assertions
assert_equal 201, response.status
assert_equal reservations_count + 1, Reservation.count
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 1, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# subscription assertions
assert_equal 0, @vlonchamp.subscriptions.count
assert_nil @vlonchamp.subscribed_plan
# reservation assertions
reservation = Reservation.last
assert reservation.original_invoice
assert_equal 1, reservation.original_invoice.invoice_items.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert_equal machine.prices.find_by(group_id: @vlonchamp.group_id, plan_id: nil).amount, invoice_item.amount
assert invoice_item.check_footprint
# invoice assertions
item = InvoiceItem.find_by(object: reservation)
invoice = item.invoice
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert_not invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: reservation)
# wallet
assert_equal 0, @vlonchamp.wallet.amount
assert_equal 2, @vlonchamp.wallet.wallet_transactions.count
transaction = @vlonchamp.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal 10, transaction.amount
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end
end

View File

@ -9,6 +9,10 @@ class StatisticServiceTest < ActionDispatch::IntegrationTest
login_as(@admin, scope: :user)
end
test 'build default stats' do
::Statistics::BuilderService.generate_statistic
end
test 'build stats' do
# Create a reservation to generate an invoice
machine = Machine.find(1)