1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-29 18:52:22 +01:00

Merge branch 'dev' for release 5.3.2

This commit is contained in:
Sylvain 2022-01-19 15:40:37 +01:00
commit f59a01d370
26 changed files with 273 additions and 70 deletions

View File

@ -1,5 +1,17 @@
# Changelog Fab-manager
# v5.3.2 2022 January 19
- Add a test for statistics generation
- Fix a bug: missing the Other payment method
- Fix a bug: do not display an untranslated string if a prepaid pack has no maximum validity
- Fix a bug: statistics not built for instances with plans created before v4.3.3
- Fix a bug: when requesting to send the sso migration code, the email was case-sensitive
- Fix a bug: the adminsys email was case-sensitive
- Fix a bug: members are unable to buy prepaid-packs by wallet
- Fix a bug: prepaid-packs without expiration date do not work
- [TODO DEPLOY] `rails fablab:maintenance:regenerate_statistics[2020,04]`
# v5.3.1 2022 January 17
- Definition of extended prices for spaces is now made in hours (previously in minutes)

View File

@ -52,10 +52,9 @@ class API::AuthProvidersController < API::ApiController
@previous = AuthProvider.previous
end
def send_code
authorize AuthProvider
user = User.find_by(email: params[:email])
user = User.find_by('lower(email) = ?', params[:email]&.downcase)
if user&.auth_token
if AuthProvider.active.providable_type != DatabaseProvider.name

View File

@ -14,7 +14,7 @@ import { useTranslation } from 'react-i18next';
declare const Application: IApplication;
interface PaymentModalProps {
interface CardPaymentModalProps {
isOpen: boolean,
toggleModal: () => void,
afterSuccess: (result: Invoice|PaymentSchedule) => void,
@ -29,7 +29,7 @@ interface PaymentModalProps {
* This component open a modal dialog for the configured payment gateway, allowing the user to input his card data
* to process an online payment.
*/
const PaymentModalComponent: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
const CardPaymentModalComponent: React.FC<CardPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
const { t } = useTranslation('shared');
const [gateway, setGateway] = useState<Setting>(null);
@ -89,12 +89,12 @@ const PaymentModalComponent: React.FC<PaymentModalProps> = ({ isOpen, toggleModa
}
};
export const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
export const CardPaymentModal: React.FC<CardPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
return (
<Loader>
<PaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} />
<CardPaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} />
</Loader>
);
};
Application.Components.component('paymentModal', react2angular(PaymentModal, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'customer']));
Application.Components.component('cardPaymentModal', react2angular(CardPaymentModal, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'customer']));

View File

@ -6,7 +6,7 @@ import LocalPaymentAPI from '../../../api/local-payment';
import FormatLib from '../../../lib/format';
import SettingAPI from '../../../api/setting';
import { SettingName } from '../../../models/setting';
import { PaymentModal } from '../payment-modal';
import { CardPaymentModal } from '../card-payment-modal';
import { PaymentSchedule } from '../../../models/payment-schedule';
import { HtmlTranslate } from '../../base/html-translate';
@ -147,7 +147,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
})}
</ul>
</div>
<PaymentModal isOpen={onlinePaymentModal}
<CardPaymentModal isOpen={onlinePaymentModal}
toggleModal={toggleOnlinePaymentModal}
afterSuccess={afterCreatePaymentSchedule}
onError={onError}

View File

@ -3,7 +3,7 @@ import { AbstractPaymentModal, GatewayFormProps } from '../abstract-payment-moda
import { LocalPaymentForm } from './local-payment-form';
import { ShoppingCart } from '../../../models/payment';
import { PaymentSchedule } from '../../../models/payment-schedule';
import { User, UserRole } from '../../../models/user';
import { User } from '../../../models/user';
import { Invoice } from '../../../models/invoice';
import { useTranslation } from 'react-i18next';
import { ModalSize } from '../../base/fab-modal';

View File

@ -25,7 +25,7 @@ interface PayZenModalProps {
* This component enables the user to input his card data or process payments, using the PayZen gateway.
* Supports Strong-Customer Authentication (SCA).
*
* This component should not be called directly. Prefer using <PaymentModal> which can handle the configuration
* This component should not be called directly. Prefer using <CardPaymentModal> which can handle the configuration
* of a different payment gateway.
*/
export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, currentUser, schedule, customer }) => {

View File

@ -0,0 +1,95 @@
import { Invoice } from '../../../models/invoice';
import { PaymentSchedule } from '../../../models/payment-schedule';
import { ShoppingCart } from '../../../models/payment';
import { User } from '../../../models/user';
import React, { useEffect, useState } from 'react';
import WalletAPI from '../../../api/wallet';
import { Wallet } from '../../../models/wallet';
import WalletLib from '../../../lib/wallet';
import UserLib from '../../../lib/user';
import { LocalPaymentModal } from '../local-payment/local-payment-modal';
import { CardPaymentModal } from '../card-payment-modal';
import PriceAPI from '../../../api/price';
import { ComputePriceResult } from '../../../models/price';
interface PaymentModalProps {
isOpen: boolean,
toggleModal: () => void,
afterSuccess: (result: Invoice|PaymentSchedule) => void,
onError: (message: string) => void,
cart: ShoppingCart,
updateCart: (cart: ShoppingCart) => void,
operator: User,
schedule?: PaymentSchedule,
customer: User
}
/**
* This component is responsible for rendering the payment modal.
*/
export const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, updateCart, operator, schedule, customer }) => {
// the user's wallet
const [wallet, setWallet] = useState<Wallet>(null);
// the price of the cart
const [price, setPrice] = useState<ComputePriceResult>(null);
// the remaining price to pay, after the wallet was changed
const [remainingPrice, setRemainingPrice] = useState<number>(null);
// refresh the wallet when the customer changes
useEffect(() => {
WalletAPI.getByUser(customer.id).then(wallet => {
setWallet(wallet);
});
}, [customer]);
// refresh the price when the cart changes
useEffect(() => {
PriceAPI.compute(cart).then(price => {
setPrice(price);
});
}, [cart]);
// refresh the remaining price when the cart price was computed and the wallet was retrieved
useEffect(() => {
if (price && wallet) {
setRemainingPrice(new WalletLib(wallet).computeRemainingPrice(price?.price));
}
}, [price, wallet]);
/**
* Check the conditions for the local payment
*/
const isLocalPayment = (): boolean => {
return (new UserLib(operator).isPrivileged(customer) || remainingPrice === 0);
};
// do not render the modal until the real remaining price is computed
if (remainingPrice === null) return null;
if (isLocalPayment()) {
return (
<LocalPaymentModal isOpen={isOpen}
toggleModal={toggleModal}
afterSuccess={afterSuccess}
onError={onError}
cart={cart}
updateCart={updateCart}
currentUser={operator}
customer={customer}
schedule={schedule}
/>
);
} else {
return (
<CardPaymentModal isOpen={isOpen}
toggleModal={toggleModal}
afterSuccess={afterSuccess}
onError={onError}
cart={cart}
currentUser={operator}
customer={customer}
schedule={schedule}
/>
);
}
};

View File

@ -26,7 +26,7 @@ interface StripeModalProps {
* This component enables the user to input his card data or process payments, using the Stripe gateway.
* Supports Strong-Customer Authentication (SCA).
*
* This component should not be called directly. Prefer using <PaymentModal> which can handle the configuration
* This component should not be called directly. Prefer using <CardPaymentModal> which can handle the configuration
* of a different payment gateway.
*/
export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, currentUser, schedule, customer }) => {

View File

@ -9,10 +9,8 @@ import { FabButton } from '../base/fab-button';
import PriceAPI from '../../api/price';
import { Price } from '../../models/price';
import { PaymentMethod, ShoppingCart } from '../../models/payment';
import { PaymentModal } from '../payment/payment-modal';
import UserLib from '../../lib/user';
import { LocalPaymentModal } from '../payment/local-payment/local-payment-modal';
import FormatLib from '../../lib/format';
import { PaymentModal } from '../payment/stripe/payment-modal';
type PackableItem = Machine;
@ -38,7 +36,6 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
const [packs, setPacks] = useState<Array<PrepaidPack>>(null);
const [cart, setCart] = useState<ShoppingCart>(null);
const [paymentModal, setPaymentModal] = useState<boolean>(false);
const [localPaymentModal, setLocalPaymentModal] = useState<boolean>(false);
useEffect(() => {
PrepaidPackAPI.index({ priceable_id: item.id, priceable_type: itemType, group_id: customer.group_id, disabled: false })
@ -56,13 +53,6 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
setPaymentModal(!paymentModal);
};
/**
* Open/closes the local payment modal (for admins and managers)
*/
const toggleLocalPaymentModal = (): void => {
setLocalPaymentModal(!localPaymentModal);
};
/**
* Convert the hourly-based price of the given prive, to a total price, based on the duration of the given pack
*/
@ -82,6 +72,8 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
* Return a user-friendly string for the validity of the provided pack
*/
const formatValidity = (pack: PrepaidPack): string => {
if (!pack.validity_interval) return null;
const period = t(`app.logged.propose_packs_modal.period.${pack.validity_interval}`, { COUNT: pack.validity_count });
return t('app.logged.propose_packs_modal.validity', { COUNT: pack.validity_count, PERIODS: period });
};
@ -105,9 +97,6 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
{ prepaid_pack: { id: pack.id } }
]
});
if (new UserLib(operator).isPrivileged(customer)) {
return toggleLocalPaymentModal();
}
togglePaymentModal();
};
};
@ -157,16 +146,9 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
afterSuccess={handlePackBought}
onError={onError}
cart={cart}
currentUser={operator}
customer={customer} />
<LocalPaymentModal isOpen={localPaymentModal}
toggleModal={toggleLocalPaymentModal}
afterSuccess={handlePackBought}
onError={onError}
cart={cart}
updateCart={setCart}
currentUser={operator}
customer={customer} />
operator={operator}
customer={customer}
updateCart={setCart} />
</div>}
</FabModal>
);

View File

@ -35,7 +35,6 @@ interface RenewModalProps {
* Modal dialog shown to renew the current subscription of a customer, for free
*/
const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscription, customer, operator, onError, onSuccess }) => {
// we do not render the modal if the subscription was not provided
if (!subscription) return null;

View File

@ -8,7 +8,7 @@ export default class WalletLib {
}
/**
* Return the price remaining to pay, after we have used the maximum possible amount in the wallet
* Return the price remaining to pay, after we have used the maximum possible amount in the wallet.
*/
computeRemainingPrice = (price: number): number => {
if (this.wallet.amount > price) {

View File

@ -19,7 +19,8 @@ export interface IntentConfirmation {
export enum PaymentMethod {
Card = 'card',
Check = 'check',
Transfer = 'transfer'
Transfer = 'transfer',
Other = ''
}
export type CartItem = { reservation: Reservation }|

View File

@ -206,13 +206,13 @@
</div>
<div ng-if="onlinePayment.showModal">
<payment-modal is-open="onlinePayment.showModal"
toggle-modal="toggleOnlinePaymentModal"
after-success="afterOnlinePaymentSuccess"
on-error="onOnlinePaymentError"
cart="onlinePayment.cartItems"
current-user="currentUser"
customer="ctrl.member"/>
<card-payment-modal is-open="onlinePayment.showModal"
toggle-modal="toggleOnlinePaymentModal"
after-success="afterOnlinePaymentSuccess"
on-error="onOnlinePaymentError"
cart="onlinePayment.cartItems"
current-user="currentUser"
customer="ctrl.member"/>
</div>
</div>

View File

@ -200,14 +200,14 @@
</div>
<div ng-if="onlinePayment.showModal">
<payment-modal is-open="onlinePayment.showModal"
toggle-modal="toggleOnlinePaymentModal"
after-success="afterOnlinePaymentSuccess"
on-error="onOnlinePaymentError"
cart="onlinePayment.cartItems"
current-user="currentUser"
customer="user"
schedule="schedule.payment_schedule"/>
<card-payment-modal is-open="onlinePayment.showModal"
toggle-modal="toggleOnlinePaymentModal"
after-success="afterOnlinePaymentSuccess"
on-error="onOnlinePaymentError"
cart="onlinePayment.cartItems"
current-user="currentUser"
customer="user"
schedule="schedule.payment_schedule"/>
</div>
<div ng-if="localPayment.showModal">

View File

@ -49,13 +49,13 @@
<div class="modal-footer">
<button class="btn btn-info" ng-click="ok()" ng-disabled="attempting" ng-bind-html="validButtonName"></button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
<payment-modal is-open="isOpenOnlinePaymentModal"
toggle-modal="toggleOnlinePaymentModal"
after-success="afterCreatePaymentSchedule"
on-error="onCreatePaymentScheduleError"
cart="cartItems"
current-user="currentUser"
schedule="schedule"
customer="user"
processPayment="false"/>
<card-payment-modal is-open="isOpenOnlinePaymentModal"
toggle-modal="toggleOnlinePaymentModal"
after-success="afterCreatePaymentSchedule"
on-error="onCreatePaymentScheduleError"
cart="cartItems"
current-user="currentUser"
schedule="schedule"
customer="user"
processPayment="false"/>
</div>

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module StatConcern
extend ActiveSupport::Concern
@ -12,7 +14,7 @@ module StatConcern
attribute :group, String
# has include Elasticsearch::Persistence::Model
index_name "stats"
document_type self.to_s.demodulize.underscore
index_name 'stats'
document_type to_s.demodulize&.underscore
end
end

View File

@ -18,7 +18,9 @@ class PrepaidPack < ApplicationRecord
validates :amount, :group_id, :priceable_id, :priceable_type, :minutes, presence: true
def validity
validity_count.send(validity_interval)
return nil if validity_interval.nil?
validity_count&.send(validity_interval)
end
def destroyable?

View File

@ -14,6 +14,8 @@ class StatisticProfilePrepaidPack < ApplicationRecord
private
def set_expiration_date
return unless prepaid_pack.validity
self.expires_at = DateTime.current + prepaid_pack.validity
end
end

View File

@ -121,7 +121,7 @@ class User < ApplicationRecord
def self.adminsys
return unless Rails.application.secrets.adminsys_email.present?
User.find_by(email: Rails.application.secrets.adminsys_email)
User.find_by('lower(email) = ?', Rails.application.secrets.adminsys_email&.downcase)
end
def training_machine?(machine)

View File

@ -24,7 +24,7 @@ class PrepaidPackService
.includes(:prepaid_pack)
.references(:prepaid_packs)
.where('statistic_profile_id = ?', user.statistic_profile.id)
.where('expires_at > ?', DateTime.current)
.where('expires_at > ? OR expires_at IS NULL', DateTime.current)
.where('prepaid_packs.priceable_id = ?', priceable.id)
.where('prepaid_packs.priceable_type = ?', priceable.class.name)
.where('minutes_used < prepaid_packs.minutes')

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
# From Fab-manager v4.3.3, the behavior of ActiveSupport::Duration#to_i has changed.
# Previously, a month = 30 days, from since a month = 30.436875 days.
# Also, previously a year = 365.25 days, from since a year = 365.2425 days.
# This introduced a bug due to the key of the statistic types for subscriptions were equal to
# the number of seconds of the plan duration, but this duration has changed due to the
# change reported above.
# This migration fixes the problem by changing the key of the statistic types for subscriptions
# to the new plans durations.
class FixSubscriptionStatisticTypes < ActiveRecord::Migration[5.2]
def up
one_month = 2_592_000
(1..12).each do |n|
StatisticType.where(key: (one_month * n).to_s).update_all(key: n.months.to_i)
end
one_year = 31_557_600
(1..10).each do |n|
StatisticType.where(key: (one_year * n).to_s).update_all(key: n.years.to_i)
end
end
def down
one_month = 2_592_000
(1..12).each do |n|
StatisticType.where(key: n.months.to_i.to_s).update_all(key: (one_month * n).to_i)
end
one_year = 31_557_600
(1..10).each do |n|
StatisticType.where(key: n.years.to_i).update_all(key: (one_year * n).to_s)
end
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_01_11_134253) do
ActiveRecord::Schema.define(version: 2022_01_18_123741) do
# These are extensions that must be enabled in order to support this database
enable_extension "fuzzystrmatch"

View File

@ -1,6 +1,6 @@
{
"name": "fab-manager",
"version": "5.3.1",
"version": "5.3.2",
"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

@ -179,3 +179,13 @@ availability_18:
updated_at: 2017-02-15 15:53:35.154433000 Z
nb_total_places: 5
destroying: false
availability_19:
id: 19
start_at: <%= 1.day.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %>
end_at: <%= 1.day.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %>
available_type: machines
created_at: 2017-02-15 15:53:35.154433000 Z
updated_at: 2017-02-15 15:53:35.154433000 Z
nb_total_places:
destroying: false

View File

@ -103,3 +103,8 @@ machines_availability_21:
id: 21
machine_id: 2
availability_id: 16
machines_availability_22:
id: 22
machine_id: 1
availability_id: 19

View File

@ -0,0 +1,61 @@
# frozen_string_literal: true
require 'test_helper'
class StatisticServiceTest < ActiveSupport::TestCase
setup do
@user = User.members.without_subscription.first
@admin = User.with_role(:admin).first
login_as(@admin, scope: :user)
end
def test
machine_stats_count = Stats::Machine.all.count
subscription_stats_count = Stats::Subscription.all.count
# Create a reservation to generate an invoice
machine = Machine.find(1)
availability = Availability.find(19)
post '/api/local_payment/confirm_payment', params: {
customer_id: @user.id,
items: [
{
reservation: {
reservable_id: machine.id,
reservable_type: machine.class.name,
slots_attributes: [
{
start_at: availability.start_at.to_s(:iso8601),
end_at: (availability.start_at + 1.hour).to_s(:iso8601),
availability_id: availability.id
}
]
}
}
]
}.to_json, headers: default_headers
# Create a subscription to generate another invoice
plan = Plan.find_by(group_id: @user.group.id, type: 'Plan')
post '/api/local_payment/confirm_payment',
params: {
customer_id: @user.id,
items: [
{
subscription: {
plan_id: plan.id
}
}
]
}.to_json, headers: default_headers
# Build the stats for today, we expect the above invoices (reservation+subscription) to appear in the resulting stats
StatisticService.new.generate_statistic(
start_date: DateTime.current.beginning_of_day,
end_date: DateTime.current.end_of_day
)
assert_equal machine_stats_count + 1, Stats::Machine.all.count
assert_equal subscription_stats_count + 1, Stats::Subscription.all.count
end
end