mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-21 15:54:22 +01:00
(feat) check cart item amount/available/quantity_min/stock before checkout
This commit is contained in:
parent
040f74a2fe
commit
ac671ea26d
@ -37,6 +37,18 @@ class API::CartController < API::ApiController
|
|||||||
render 'api/orders/show'
|
render 'api/orders/show'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def refresh_item
|
||||||
|
authorize @current_order, policy_class: CartPolicy
|
||||||
|
@order = Cart::RefreshItemService.new.call(@current_order, orderable)
|
||||||
|
render 'api/orders/show'
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate
|
||||||
|
authorize @current_order, policy_class: CartPolicy
|
||||||
|
@order_errors = Cart::CheckCartService.new.call(@current_order)
|
||||||
|
render json: @order_errors
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def orderable
|
def orderable
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import apiClient from './clients/api-client';
|
import apiClient from './clients/api-client';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { Order } from '../models/order';
|
import { Order, OrderErrors } from '../models/order';
|
||||||
|
|
||||||
export default class CartAPI {
|
export default class CartAPI {
|
||||||
static async create (token?: string): Promise<Order> {
|
static async create (token?: string): Promise<Order> {
|
||||||
@ -27,4 +27,14 @@ export default class CartAPI {
|
|||||||
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/set_offer', { order_token: order.token, orderable_id: orderableId, is_offered: isOffered });
|
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/set_offer', { order_token: order.token, orderable_id: orderableId, is_offered: isOffered });
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async refreshItem (order: Order, orderableId: number): Promise<Order> {
|
||||||
|
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/refresh_item', { order_token: order.token, orderable_id: orderableId });
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async validate (order: Order): Promise<OrderErrors> {
|
||||||
|
const res: AxiosResponse<OrderErrors> = await apiClient.post('/api/cart/validate', { order_token: order.token });
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import CartAPI from '../../api/cart';
|
|||||||
import { User } from '../../models/user';
|
import { User } from '../../models/user';
|
||||||
import { PaymentModal } from '../payment/stripe/payment-modal';
|
import { PaymentModal } from '../payment/stripe/payment-modal';
|
||||||
import { PaymentMethod } from '../../models/payment';
|
import { PaymentMethod } from '../../models/payment';
|
||||||
import { Order } from '../../models/order';
|
import { Order, OrderErrors } from '../../models/order';
|
||||||
import { MemberSelect } from '../user/member-select';
|
import { MemberSelect } from '../user/member-select';
|
||||||
import { CouponInput } from '../coupon/coupon-input';
|
import { CouponInput } from '../coupon/coupon-input';
|
||||||
import { Coupon } from '../../models/coupon';
|
import { Coupon } from '../../models/coupon';
|
||||||
@ -18,6 +18,7 @@ import noImage from '../../../../images/no_image.png';
|
|||||||
import Switch from 'react-switch';
|
import Switch from 'react-switch';
|
||||||
import OrderLib from '../../lib/order';
|
import OrderLib from '../../lib/order';
|
||||||
import { CaretDown, CaretUp } from 'phosphor-react';
|
import { CaretDown, CaretUp } from 'phosphor-react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
declare const Application: IApplication;
|
declare const Application: IApplication;
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
const { t } = useTranslation('public');
|
const { t } = useTranslation('public');
|
||||||
|
|
||||||
const { cart, setCart } = useCart(currentUser);
|
const { cart, setCart } = useCart(currentUser);
|
||||||
|
const [cartErrors, setCartErrors] = useState<OrderErrors>(null);
|
||||||
const [itemsQuantity, setItemsQuantity] = useState<{ id: number; quantity: number; }[]>();
|
const [itemsQuantity, setItemsQuantity] = useState<{ id: number; quantity: number; }[]>();
|
||||||
const [paymentModal, setPaymentModal] = useState<boolean>(false);
|
const [paymentModal, setPaymentModal] = useState<boolean>(false);
|
||||||
|
|
||||||
@ -54,6 +56,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
CartAPI.removeItem(cart, item.orderable_id).then(data => {
|
CartAPI.removeItem(cart, item.orderable_id).then(data => {
|
||||||
setCart(data);
|
setCart(data);
|
||||||
|
checkCart();
|
||||||
}).catch(onError);
|
}).catch(onError);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -65,6 +68,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
CartAPI.setQuantity(cart, item.orderable_id, e.target.value)
|
CartAPI.setQuantity(cart, item.orderable_id, e.target.value)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setCart(data);
|
setCart(data);
|
||||||
|
checkCart();
|
||||||
})
|
})
|
||||||
.catch(() => onError(t('app.public.store_cart.stock_limit')));
|
.catch(() => onError(t('app.public.store_cart.stock_limit')));
|
||||||
};
|
};
|
||||||
@ -73,10 +77,34 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
CartAPI.setQuantity(cart, item.orderable_id, direction === 'up' ? item.quantity + 1 : item.quantity - 1)
|
CartAPI.setQuantity(cart, item.orderable_id, direction === 'up' ? item.quantity + 1 : item.quantity - 1)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setCart(data);
|
setCart(data);
|
||||||
|
checkCart();
|
||||||
})
|
})
|
||||||
.catch(() => onError(t('app.public.store_cart.stock_limit')));
|
.catch(() => onError(t('app.public.store_cart.stock_limit')));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh product amount
|
||||||
|
*/
|
||||||
|
const refreshItem = (item) => {
|
||||||
|
return (e: React.BaseSyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
CartAPI.refreshItem(cart, item.orderable_id).then(data => {
|
||||||
|
setCart(data);
|
||||||
|
checkCart();
|
||||||
|
}).catch(onError);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the current cart's items (available, price, stock, quantity_min)
|
||||||
|
*/
|
||||||
|
const checkCart = async (): Promise<OrderErrors> => {
|
||||||
|
const errors = await CartAPI.validate(cart);
|
||||||
|
setCartErrors(errors);
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checkout cart
|
* Checkout cart
|
||||||
*/
|
*/
|
||||||
@ -84,10 +112,35 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
userLogin();
|
userLogin();
|
||||||
} else {
|
} else {
|
||||||
setPaymentModal(true);
|
checkCart().then(errors => {
|
||||||
|
if (!hasCartErrors(errors)) {
|
||||||
|
setPaymentModal(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the carrent cart has any error
|
||||||
|
*/
|
||||||
|
const hasCartErrors = (errors: OrderErrors) => {
|
||||||
|
if (!errors) return false;
|
||||||
|
for (const item of cart.order_items_attributes) {
|
||||||
|
const error = _.find(errors.details, (e) => e.item_id === item.id);
|
||||||
|
if (!error || error?.errors?.length > 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get givean item's error
|
||||||
|
*/
|
||||||
|
const getItemErrors = (item) => {
|
||||||
|
if (!cartErrors) return [];
|
||||||
|
const errors = _.find(cartErrors.details, (e) => e.item_id === item.id);
|
||||||
|
return errors.errors || [{ error: 'not_found' }];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open/closes the payment modal
|
* Open/closes the payment modal
|
||||||
*/
|
*/
|
||||||
@ -149,12 +202,36 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show item error
|
||||||
|
*/
|
||||||
|
const itemError = (item, error) => {
|
||||||
|
if (error.error === 'is_active' || error.error === 'not_found') {
|
||||||
|
return <div className='error'>This product is not available, please remove it in cart</div>;
|
||||||
|
}
|
||||||
|
if (error.error === 'stock' && error.value === 0) {
|
||||||
|
return <div className='error'>This product is out of stock. Please remove this item to before checkout the cart.</div>;
|
||||||
|
}
|
||||||
|
if (error.error === 'stock' && error.value > 0) {
|
||||||
|
return <div className='error'>This product has only {error.value} unit in stock, please change the quantity of item.</div>;
|
||||||
|
}
|
||||||
|
if (error.error === 'quantity_min') {
|
||||||
|
return <div className='error'>Minimum number of product was changed to {error.value}, please change the quantity of item</div>;
|
||||||
|
}
|
||||||
|
if (error.error === 'amount') {
|
||||||
|
return <div className='error'>
|
||||||
|
The price of product was modified to {FormatLib.price(error.value)} / {t('app.public.store_cart.unit')}
|
||||||
|
<span onClick={refreshItem(item)}>Update</span>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='store-cart'>
|
<div className='store-cart'>
|
||||||
<div className="store-cart-list">
|
<div className="store-cart-list">
|
||||||
{cart && cartIsEmpty() && <p>{t('app.public.store_cart.cart_is_empty')}</p>}
|
{cart && cartIsEmpty() && <p>{t('app.public.store_cart.cart_is_empty')}</p>}
|
||||||
{cart && cart.order_items_attributes.map(item => (
|
{cart && cart.order_items_attributes.map(item => (
|
||||||
<article key={item.id} className='store-cart-list-item'>
|
<article key={item.id} className={`store-cart-list-item ${getItemErrors(item).length > 0 ? 'error' : ''}`}>
|
||||||
<div className='picture'>
|
<div className='picture'>
|
||||||
<img alt=''src={item.orderable_main_image_url || noImage} />
|
<img alt=''src={item.orderable_main_image_url || noImage} />
|
||||||
</div>
|
</div>
|
||||||
@ -164,6 +241,11 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
{item.quantity_min > 1 &&
|
{item.quantity_min > 1 &&
|
||||||
<span className='min'>{t('app.public.store_cart.minimum_purchase')}{item.quantity_min}</span>
|
<span className='min'>{t('app.public.store_cart.minimum_purchase')}{item.quantity_min}</span>
|
||||||
}
|
}
|
||||||
|
<div>
|
||||||
|
{getItemErrors(item).map(e => {
|
||||||
|
return itemError(item, e);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className='price'>
|
<div className='price'>
|
||||||
|
@ -63,3 +63,14 @@ export interface OrderIndexFilter extends ApiFilter {
|
|||||||
period_from?: string,
|
period_from?: string,
|
||||||
period_to?: string
|
period_to?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OrderErrors {
|
||||||
|
order_id: number,
|
||||||
|
details: Array<{
|
||||||
|
item_id: number,
|
||||||
|
errors: Array<{
|
||||||
|
error: string,
|
||||||
|
value: string|number
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
@include text-base(600);
|
@include text-base(600);
|
||||||
}
|
}
|
||||||
.min {
|
.min,.error {
|
||||||
margin-top: 0.8rem;
|
margin-top: 0.8rem;
|
||||||
color: var(--alert);
|
color: var(--alert);
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
@ -133,6 +133,9 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.error {
|
||||||
|
border-color: var(--alert);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.group {
|
.group {
|
||||||
|
@ -6,7 +6,7 @@ class CartPolicy < ApplicationPolicy
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
%w[add_item remove_item set_quantity].each do |action|
|
%w[add_item remove_item set_quantity refresh_item validate].each do |action|
|
||||||
define_method "#{action}?" do
|
define_method "#{action}?" do
|
||||||
return user.privileged? || (record.statistic_profile_id == user.statistic_profile.id) if user
|
return user.privileged? || (record.statistic_profile_id == user.statistic_profile.id) if user
|
||||||
|
|
||||||
|
18
app/services/cart/check_cart_service.rb
Normal file
18
app/services/cart/check_cart_service.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Provides methods for check cart's items (available, price, stock, quantity_min)
|
||||||
|
class Cart::CheckCartService
|
||||||
|
def call(order)
|
||||||
|
res = { order_id: order.id, details: [] }
|
||||||
|
order.order_items.each do |item|
|
||||||
|
errors = []
|
||||||
|
errors.push({ error: 'is_active', value: false }) unless item.orderable.is_active
|
||||||
|
errors.push({ error: 'stock', value: item.orderable.stock['external'] }) if item.quantity > item.orderable.stock['external']
|
||||||
|
orderable_amount = item.orderable.amount || 0
|
||||||
|
errors.push({ error: 'amount', value: orderable_amount / 100.0 }) if item.amount != orderable_amount
|
||||||
|
errors.push({ error: 'quantity_min', value: item.orderable.quantity_min }) if item.quantity < item.orderable.quantity_min
|
||||||
|
res[:details].push({ item_id: item.id, errors: errors })
|
||||||
|
end
|
||||||
|
res
|
||||||
|
end
|
||||||
|
end
|
21
app/services/cart/refresh_item_service.rb
Normal file
21
app/services/cart/refresh_item_service.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Provides methods for refresh amount of order item
|
||||||
|
class Cart::RefreshItemService
|
||||||
|
def call(order, orderable)
|
||||||
|
raise Cart::InactiveProductError unless orderable.is_active
|
||||||
|
|
||||||
|
item = order.order_items.find_by(orderable: orderable)
|
||||||
|
|
||||||
|
raise ActiveRecord::RecordNotFound if item.nil?
|
||||||
|
|
||||||
|
order.total -= (item.amount * item.quantity.to_i) unless item.is_offered
|
||||||
|
item.amount = orderable.amount || 0
|
||||||
|
order.total += (item.amount * item.quantity.to_i) unless item.is_offered
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
item.save
|
||||||
|
order.save
|
||||||
|
end
|
||||||
|
order.reload
|
||||||
|
end
|
||||||
|
end
|
@ -58,7 +58,8 @@ class Orders::OrderService
|
|||||||
|
|
||||||
def item_amount_not_equal?(order)
|
def item_amount_not_equal?(order)
|
||||||
order.order_items.each do |item|
|
order.order_items.each do |item|
|
||||||
return false if item.amount != item.orderable.amount
|
orderable_amount = item.orderable.amount || 0
|
||||||
|
return false if item.amount != orderable_amount
|
||||||
end
|
end
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -162,6 +162,8 @@ Rails.application.routes.draw do
|
|||||||
put 'remove_item', on: :collection
|
put 'remove_item', on: :collection
|
||||||
put 'set_quantity', on: :collection
|
put 'set_quantity', on: :collection
|
||||||
put 'set_offer', on: :collection
|
put 'set_offer', on: :collection
|
||||||
|
put 'refresh_item', on: :collection
|
||||||
|
post 'validate', on: :collection
|
||||||
end
|
end
|
||||||
resources :checkout, only: %i[] do
|
resources :checkout, only: %i[] do
|
||||||
post 'payment', on: :collection
|
post 'payment', on: :collection
|
||||||
|
20
db/schema.rb
20
db/schema.rb
@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2022_09_20_131912) do
|
|||||||
enable_extension "unaccent"
|
enable_extension "unaccent"
|
||||||
|
|
||||||
create_table "abuses", id: :serial, force: :cascade do |t|
|
create_table "abuses", id: :serial, force: :cascade do |t|
|
||||||
t.integer "signaled_id"
|
|
||||||
t.string "signaled_type"
|
t.string "signaled_type"
|
||||||
|
t.integer "signaled_id"
|
||||||
t.string "first_name"
|
t.string "first_name"
|
||||||
t.string "last_name"
|
t.string "last_name"
|
||||||
t.string "email"
|
t.string "email"
|
||||||
@ -49,8 +49,8 @@ ActiveRecord::Schema.define(version: 2022_09_20_131912) do
|
|||||||
t.string "locality"
|
t.string "locality"
|
||||||
t.string "country"
|
t.string "country"
|
||||||
t.string "postal_code"
|
t.string "postal_code"
|
||||||
t.integer "placeable_id"
|
|
||||||
t.string "placeable_type"
|
t.string "placeable_type"
|
||||||
|
t.integer "placeable_id"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
end
|
end
|
||||||
@ -64,8 +64,8 @@ ActiveRecord::Schema.define(version: 2022_09_20_131912) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "assets", id: :serial, force: :cascade do |t|
|
create_table "assets", id: :serial, force: :cascade do |t|
|
||||||
t.integer "viewable_id"
|
|
||||||
t.string "viewable_type"
|
t.string "viewable_type"
|
||||||
|
t.integer "viewable_id"
|
||||||
t.string "attachment"
|
t.string "attachment"
|
||||||
t.string "type"
|
t.string "type"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
@ -147,8 +147,8 @@ ActiveRecord::Schema.define(version: 2022_09_20_131912) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "credits", id: :serial, force: :cascade do |t|
|
create_table "credits", id: :serial, force: :cascade do |t|
|
||||||
t.integer "creditable_id"
|
|
||||||
t.string "creditable_type"
|
t.string "creditable_type"
|
||||||
|
t.integer "creditable_id"
|
||||||
t.integer "plan_id"
|
t.integer "plan_id"
|
||||||
t.integer "hours"
|
t.integer "hours"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
@ -375,15 +375,15 @@ ActiveRecord::Schema.define(version: 2022_09_20_131912) do
|
|||||||
|
|
||||||
create_table "notifications", id: :serial, force: :cascade do |t|
|
create_table "notifications", id: :serial, force: :cascade do |t|
|
||||||
t.integer "receiver_id"
|
t.integer "receiver_id"
|
||||||
t.integer "attached_object_id"
|
|
||||||
t.string "attached_object_type"
|
t.string "attached_object_type"
|
||||||
|
t.integer "attached_object_id"
|
||||||
t.integer "notification_type_id"
|
t.integer "notification_type_id"
|
||||||
t.boolean "is_read", default: false
|
t.boolean "is_read", default: false
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.string "receiver_type"
|
t.string "receiver_type"
|
||||||
t.boolean "is_send", default: false
|
t.boolean "is_send", default: false
|
||||||
t.jsonb "meta_data", default: {}
|
t.jsonb "meta_data", default: "{}"
|
||||||
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
|
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
|
||||||
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
||||||
end
|
end
|
||||||
@ -623,8 +623,8 @@ ActiveRecord::Schema.define(version: 2022_09_20_131912) do
|
|||||||
create_table "prices", id: :serial, force: :cascade do |t|
|
create_table "prices", id: :serial, force: :cascade do |t|
|
||||||
t.integer "group_id"
|
t.integer "group_id"
|
||||||
t.integer "plan_id"
|
t.integer "plan_id"
|
||||||
t.integer "priceable_id"
|
|
||||||
t.string "priceable_type"
|
t.string "priceable_type"
|
||||||
|
t.integer "priceable_id"
|
||||||
t.integer "amount"
|
t.integer "amount"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
@ -823,8 +823,8 @@ ActiveRecord::Schema.define(version: 2022_09_20_131912) do
|
|||||||
t.text "message"
|
t.text "message"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.integer "reservable_id"
|
|
||||||
t.string "reservable_type"
|
t.string "reservable_type"
|
||||||
|
t.integer "reservable_id"
|
||||||
t.integer "nb_reserve_places"
|
t.integer "nb_reserve_places"
|
||||||
t.integer "statistic_profile_id"
|
t.integer "statistic_profile_id"
|
||||||
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
|
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
|
||||||
@ -833,8 +833,8 @@ ActiveRecord::Schema.define(version: 2022_09_20_131912) do
|
|||||||
|
|
||||||
create_table "roles", id: :serial, force: :cascade do |t|
|
create_table "roles", id: :serial, force: :cascade do |t|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.integer "resource_id"
|
|
||||||
t.string "resource_type"
|
t.string "resource_type"
|
||||||
|
t.integer "resource_id"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
|
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
|
||||||
@ -1114,8 +1114,8 @@ ActiveRecord::Schema.define(version: 2022_09_20_131912) do
|
|||||||
t.boolean "is_allow_newsletter"
|
t.boolean "is_allow_newsletter"
|
||||||
t.inet "current_sign_in_ip"
|
t.inet "current_sign_in_ip"
|
||||||
t.inet "last_sign_in_ip"
|
t.inet "last_sign_in_ip"
|
||||||
t.string "mapped_from_sso"
|
|
||||||
t.datetime "validated_at"
|
t.datetime "validated_at"
|
||||||
|
t.string "mapped_from_sso"
|
||||||
t.index ["auth_token"], name: "index_users_on_auth_token"
|
t.index ["auth_token"], name: "index_users_on_auth_token"
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
|
Loading…
x
Reference in New Issue
Block a user