mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-29 18:52:22 +01:00
create stripe subscription with all data
This commit is contained in:
parent
ed5b90cbdc
commit
b5504d2342
@ -40,7 +40,7 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
|
|||||||
* Test if all payment deadlines have the same amount
|
* Test if all payment deadlines have the same amount
|
||||||
*/
|
*/
|
||||||
const hasEqualDeadlines = (): boolean => {
|
const hasEqualDeadlines = (): boolean => {
|
||||||
const prices = schedule.items.map(i => i.price);
|
const prices = schedule.items.map(i => i.amount);
|
||||||
return prices.every(p => p === prices[0]);
|
return prices.every(p => p === prices[0]);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -57,7 +57,7 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
|
|||||||
{hasEqualDeadlines() && <ul>
|
{hasEqualDeadlines() && <ul>
|
||||||
<li>
|
<li>
|
||||||
<span className="schedule-item-info">
|
<span className="schedule-item-info">
|
||||||
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length, AMOUNT: formatPrice(schedule.items[0].price) })}
|
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length, AMOUNT: formatPrice(schedule.items[0].amount) })}
|
||||||
</span>
|
</span>
|
||||||
<span className="schedule-item-date">{t('app.shared.cart.first_debit')}</span>
|
<span className="schedule-item-date">{t('app.shared.cart.first_debit')}</span>
|
||||||
</li>
|
</li>
|
||||||
@ -65,12 +65,12 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
|
|||||||
{!hasEqualDeadlines() && <ul>
|
{!hasEqualDeadlines() && <ul>
|
||||||
<li>
|
<li>
|
||||||
<span className="schedule-item-info">{t('app.shared.cart.monthly_payment_NUMBER', { NUMBER: 1 })}</span>
|
<span className="schedule-item-info">{t('app.shared.cart.monthly_payment_NUMBER', { NUMBER: 1 })}</span>
|
||||||
<span className="schedule-item-price">{formatPrice(schedule.items[0].price)}</span>
|
<span className="schedule-item-price">{formatPrice(schedule.items[0].amount)}</span>
|
||||||
<span className="schedule-item-date">{t('app.shared.cart.debit')}</span>
|
<span className="schedule-item-date">{t('app.shared.cart.debit')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="schedule-item-info">
|
<span className="schedule-item-info">
|
||||||
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length - 1, AMOUNT: formatPrice(schedule.items[1].price) })}
|
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length - 1, AMOUNT: formatPrice(schedule.items[1].amount) })}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>}
|
</ul>}
|
||||||
@ -81,7 +81,7 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
|
|||||||
<li key={String(item.due_date)}>
|
<li key={String(item.due_date)}>
|
||||||
<span className="schedule-item-date">{formatDate(item.due_date)}</span>
|
<span className="schedule-item-date">{formatDate(item.due_date)}</span>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<span className="schedule-item-price">{formatPrice(item.price)}</span>
|
<span className="schedule-item-price">{formatPrice(item.amount)}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -94,6 +94,7 @@ const PaymentScheduleSummaryWrapper: React.FC<PaymentScheduleSummaryProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Loader>
|
<Loader>
|
||||||
<PaymentScheduleSummary schedule={schedule} $filter={$filter} />
|
<PaymentScheduleSummary schedule={schedule} $filter={$filter} />
|
||||||
|
<div>lorem ipsum</div>
|
||||||
</Loader>
|
</Loader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
export interface PaymentScheduleItem {
|
export interface PaymentScheduleItem {
|
||||||
id: number,
|
id: number,
|
||||||
price: number,
|
amount: number,
|
||||||
due_date: Date
|
due_date: Date
|
||||||
|
details: {
|
||||||
|
recurring: number,
|
||||||
|
adjustment: number,
|
||||||
|
other_items: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaymentSchedule {
|
export interface PaymentSchedule {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
# Coupon is a textual code associated with a discount rate or an amount of discount
|
# Coupon is a textual code associated with a discount rate or an amount of discount
|
||||||
class Coupon < ApplicationRecord
|
class Coupon < ApplicationRecord
|
||||||
has_many :invoices
|
has_many :invoices
|
||||||
|
has_many :payment_schedule
|
||||||
|
|
||||||
after_commit :create_stripe_coupon, on: [:create]
|
after_commit :create_stripe_coupon, on: [:create]
|
||||||
after_commit :delete_stripe_coupon, on: [:destroy]
|
after_commit :delete_stripe_coupon, on: [:destroy]
|
||||||
|
@ -22,6 +22,7 @@ class Invoice < ApplicationRecord
|
|||||||
belongs_to :offer_day, foreign_type: 'OfferDay', foreign_key: 'invoiced_id'
|
belongs_to :offer_day, foreign_type: 'OfferDay', foreign_key: 'invoiced_id'
|
||||||
|
|
||||||
has_one :avoir, class_name: 'Invoice', foreign_key: :invoice_id, dependent: :destroy
|
has_one :avoir, class_name: 'Invoice', foreign_key: :invoice_id, dependent: :destroy
|
||||||
|
has_one :payment_schedule_item
|
||||||
belongs_to :operator_profile, foreign_key: :operator_profile_id, class_name: 'InvoicingProfile'
|
belongs_to :operator_profile, foreign_key: :operator_profile_id, class_name: 'InvoicingProfile'
|
||||||
|
|
||||||
before_create :add_environment
|
before_create :add_environment
|
||||||
|
@ -10,6 +10,7 @@ class InvoicingProfile < ApplicationRecord
|
|||||||
has_one :organization, dependent: :destroy
|
has_one :organization, dependent: :destroy
|
||||||
accepts_nested_attributes_for :organization, allow_destroy: false
|
accepts_nested_attributes_for :organization, allow_destroy: false
|
||||||
has_many :invoices, dependent: :destroy
|
has_many :invoices, dependent: :destroy
|
||||||
|
has_many :payment_schedules, dependent: :destroy
|
||||||
|
|
||||||
has_one :wallet, dependent: :destroy
|
has_one :wallet, dependent: :destroy
|
||||||
has_many :wallet_transactions, dependent: :destroy
|
has_many :wallet_transactions, dependent: :destroy
|
||||||
@ -17,6 +18,7 @@ class InvoicingProfile < ApplicationRecord
|
|||||||
has_many :history_values, dependent: :nullify
|
has_many :history_values, dependent: :nullify
|
||||||
|
|
||||||
has_many :operated_invoices, foreign_key: :operator_profile_id, class_name: 'Invoice', dependent: :nullify
|
has_many :operated_invoices, foreign_key: :operator_profile_id, class_name: 'Invoice', dependent: :nullify
|
||||||
|
has_many :operated_payment_schedules, foreign_key: :operator_profile_id, class_name: 'PaymentSchedule', dependent: :nullify
|
||||||
|
|
||||||
def full_name
|
def full_name
|
||||||
# if first_name or last_name is nil, the empty string will be used as a temporary replacement
|
# if first_name or last_name is nil, the empty string will be used as a temporary replacement
|
||||||
|
@ -8,4 +8,16 @@ class PaymentSchedule < ApplicationRecord
|
|||||||
belongs_to :coupon
|
belongs_to :coupon
|
||||||
belongs_to :invoicing_profile
|
belongs_to :invoicing_profile
|
||||||
belongs_to :operator_profile, foreign_key: :operator_profile_id, class_name: 'InvoicingProfile'
|
belongs_to :operator_profile, foreign_key: :operator_profile_id, class_name: 'InvoicingProfile'
|
||||||
|
|
||||||
|
belongs_to :subscription, foreign_type: 'Subscription', foreign_key: 'scheduled_id'
|
||||||
|
belongs_to :reservation, foreign_type: 'Reservation', foreign_key: 'scheduled_id'
|
||||||
|
|
||||||
|
has_many :payment_schedule_items
|
||||||
|
|
||||||
|
##
|
||||||
|
# This is useful to check the first item because its amount may be different from the others
|
||||||
|
##
|
||||||
|
def ordered_items
|
||||||
|
payment_schedule_items.order(due_date: :asc)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,4 +3,5 @@
|
|||||||
# Represents a due date and the associated amount for a PaymentSchedule
|
# Represents a due date and the associated amount for a PaymentSchedule
|
||||||
class PaymentScheduleItem < ApplicationRecord
|
class PaymentScheduleItem < ApplicationRecord
|
||||||
belongs_to :payment_schedule
|
belongs_to :payment_schedule
|
||||||
|
belongs_to :invoice
|
||||||
end
|
end
|
||||||
|
@ -144,10 +144,9 @@ class Price < ApplicationRecord
|
|||||||
cp = cs.validate(options[:coupon_code], user.id)
|
cp = cs.validate(options[:coupon_code], user.id)
|
||||||
total_amount = cs.apply(total_amount, cp)
|
total_amount = cs.apply(total_amount, cp)
|
||||||
|
|
||||||
# == generate PaymentSchedule ()if applicable) ===
|
# == generate PaymentSchedule (if applicable) ===
|
||||||
schedule = if options[:payment_schedule] && plan.monthly_payment
|
schedule = if options[:payment_schedule] && plan.monthly_payment
|
||||||
pss = PaymentScheduleService.new
|
PaymentScheduleService.new.compute(plan, _amount_no_coupon, cp)
|
||||||
pss.compute(plan, _amount_no_coupon, cp)
|
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
@ -18,6 +18,7 @@ class Reservation < ApplicationRecord
|
|||||||
accepts_nested_attributes_for :tickets, allow_destroy: false
|
accepts_nested_attributes_for :tickets, allow_destroy: false
|
||||||
|
|
||||||
has_one :invoice, -> { where(type: nil) }, as: :invoiced, dependent: :destroy
|
has_one :invoice, -> { where(type: nil) }, as: :invoiced, dependent: :destroy
|
||||||
|
has_one :payment_schedule, as: :scheduled, dependent: :destroy
|
||||||
|
|
||||||
validates_presence_of :reservable_id, :reservable_type
|
validates_presence_of :reservable_id, :reservable_type
|
||||||
validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
|
validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
|
||||||
|
@ -7,6 +7,7 @@ class Subscription < ApplicationRecord
|
|||||||
belongs_to :plan
|
belongs_to :plan
|
||||||
belongs_to :statistic_profile
|
belongs_to :statistic_profile
|
||||||
|
|
||||||
|
has_one :payment_schedule, as: :scheduled, dependent: :destroy
|
||||||
has_many :invoices, as: :invoiced, dependent: :destroy
|
has_many :invoices, as: :invoiced, dependent: :destroy
|
||||||
has_many :offer_days, dependent: :destroy
|
has_many :offer_days, dependent: :destroy
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ class Subscription < ApplicationRecord
|
|||||||
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
|
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
|
||||||
coupon = Coupon.find_by(code: coupon_code) unless coupon_code.nil?
|
coupon = Coupon.find_by(code: coupon_code) unless coupon_code.nil?
|
||||||
|
|
||||||
schedule = PaymentScheduleService.new.create(self, plan.amount, coupon, operator, method)
|
schedule = PaymentScheduleService.new.create(self, plan.amount, coupon: coupon, operator: operator, payment_method: method)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ class WalletTransaction < ApplicationRecord
|
|||||||
belongs_to :reservation
|
belongs_to :reservation
|
||||||
belongs_to :transactable, polymorphic: true
|
belongs_to :transactable, polymorphic: true
|
||||||
has_one :invoice
|
has_one :invoice
|
||||||
|
has_one :payment_schedule
|
||||||
|
|
||||||
validates_inclusion_of :transaction_type, in: %w[credit debit]
|
validates_inclusion_of :transaction_type, in: %w[credit debit]
|
||||||
validates :invoicing_profile, :wallet, presence: true
|
validates :invoicing_profile, :wallet, presence: true
|
||||||
|
@ -28,7 +28,10 @@ class PaymentScheduleService
|
|||||||
items = []
|
items = []
|
||||||
(0..deadlines - 1).each do |i|
|
(0..deadlines - 1).each do |i|
|
||||||
date = DateTime.current + i.months
|
date = DateTime.current + i.months
|
||||||
|
details = { recurring: per_month }
|
||||||
amount = if i.zero?
|
amount = if i.zero?
|
||||||
|
details[:adjustment] = adjustment
|
||||||
|
details[:other_items] = other_items
|
||||||
per_month + adjustment + other_items
|
per_month + adjustment + other_items
|
||||||
else
|
else
|
||||||
per_month
|
per_month
|
||||||
@ -36,13 +39,14 @@ class PaymentScheduleService
|
|||||||
items.push PaymentScheduleItem.new(
|
items.push PaymentScheduleItem.new(
|
||||||
amount: amount,
|
amount: amount,
|
||||||
due_date: date,
|
due_date: date,
|
||||||
payment_schedule: ps
|
payment_schedule: ps,
|
||||||
|
details: details
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
{ payment_schedule: ps, items: items }
|
{ payment_schedule: ps, items: items }
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(subscription, total, coupon, operator, payment_method)
|
def create(subscription, total, coupon: nil, operator: nil, payment_method: nil, reservation: nil)
|
||||||
schedule = compute(subscription.plan, total, coupon)
|
schedule = compute(subscription.plan, total, coupon)
|
||||||
ps = schedule[:payment_schedule]
|
ps = schedule[:payment_schedule]
|
||||||
items = schedule[:items]
|
items = schedule[:items]
|
||||||
@ -50,11 +54,13 @@ class PaymentScheduleService
|
|||||||
ps.scheduled = subscription
|
ps.scheduled = subscription
|
||||||
ps.payment_method = payment_method
|
ps.payment_method = payment_method
|
||||||
ps.operator_profile_id = operator
|
ps.operator_profile_id = operator
|
||||||
|
ps.save!
|
||||||
# TODO, fields: reference, wallet_amount, wallet_transaction_id, footprint, environment, invoicing_profile
|
# TODO, fields: reference, wallet_amount, wallet_transaction_id, footprint, environment, invoicing_profile
|
||||||
items.each do |item|
|
items.each do |item|
|
||||||
item.payment_schedule = ps
|
item.payment_schedule = ps
|
||||||
|
item.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
StripeWorker.perform_async(:create_stripe_subscription, ps.id)
|
StripeWorker.perform_async(:create_stripe_subscription, ps.id, reservation&.reservable&.stp_product_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -13,7 +13,7 @@ end
|
|||||||
if @amount[:schedule]
|
if @amount[:schedule]
|
||||||
json.schedule do
|
json.schedule do
|
||||||
json.items @amount[:schedule][:items] do |item|
|
json.items @amount[:schedule][:items] do |item|
|
||||||
json.price item.amount / 100.00
|
json.amount item.amount / 100.00
|
||||||
json.due_date item.due_date
|
json.due_date item.due_date
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -68,30 +68,54 @@ class StripeWorker
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_stripe_subscription(payment_schedule_id, first_invoice_items)
|
def create_stripe_subscription(payment_schedule_id, reservable_stp_id)
|
||||||
payment_schedule = PaymentSchedule.find(payment_schedule_id)
|
payment_schedule = PaymentSchedule.find(payment_schedule_id)
|
||||||
|
|
||||||
|
first_item = payment_schedule.ordered_items.first
|
||||||
|
second_item = payment_schedule.ordered_items[1]
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
first_invoice_items.each do |fii|
|
if first_item.amount != second_item.amount
|
||||||
# TODO, fill this prices with real data
|
if first_item.details[:adjustment]
|
||||||
price = Stripe::Price.create({
|
# adjustment: when dividing the price of the plan / months, sometimes it forces us to round the amount per month.
|
||||||
unit_amount: 2000,
|
# The difference is invoiced here
|
||||||
currency: 'eur',
|
p1 = Stripe::Price.create({
|
||||||
recurring: { interval: 'month' },
|
unit_amount: first_item.details[:adjustment],
|
||||||
product_data: {
|
currency: Setting.get('stripe_currency'),
|
||||||
name: 'lorem ipsum'
|
product: payment_schedule.scheduled.plan.stp_product_id,
|
||||||
}
|
nickname: "Price adjustment payment schedule #{payment_schedule_id}"
|
||||||
},
|
}, { api_key: Setting.get('stripe_secret_key') })
|
||||||
{ api_key: Setting.get('stripe_secret_key') })
|
items.push(price: p1[:id])
|
||||||
items.push(price: price[:id])
|
end
|
||||||
|
if first_item.details[:other_items]
|
||||||
|
# when taking a subscription at the same time of a reservation (space, machine or training), the amount of the
|
||||||
|
# reservation is invoiced here.
|
||||||
|
p2 = Stripe::Price.create({
|
||||||
|
unit_amount: first_item.details[:other_items],
|
||||||
|
currency: Setting.get('stripe_currency'),
|
||||||
|
product: reservable_stp_id,
|
||||||
|
nickname: "Reservations for payment schedule #{payment_schedule_id}"
|
||||||
|
}, { api_key: Setting.get('stripe_secret_key') })
|
||||||
|
items.push(price: p2[:id])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# subscription (recurring price)
|
||||||
|
price = Stripe::Price.create({
|
||||||
|
unit_amount: first_item.details[:recurring],
|
||||||
|
currency: Setting.get('stripe_currency'),
|
||||||
|
recurring: { interval: 'month', interval_count: 1 },
|
||||||
|
product: payment_schedule.scheduled.plan.stp_product_id
|
||||||
|
},
|
||||||
|
{ api_key: Setting.get('stripe_secret_key') })
|
||||||
|
|
||||||
stp_subscription = Stripe::Subscription.create({
|
stp_subscription = Stripe::Subscription.create({
|
||||||
customer: payment_schedule.invoicing_profile.user.stp_customer_id,
|
customer: payment_schedule.invoicing_profile.user.stp_customer_id,
|
||||||
cancel_at: payment_schedule.scheduled.expiration_date,
|
cancel_at: payment_schedule.scheduled.expiration_date,
|
||||||
promotion_code: payment_schedule.coupon&.code,
|
promotion_code: payment_schedule.coupon&.code,
|
||||||
add_invoice_items: items,
|
add_invoice_items: items,
|
||||||
items: [
|
items: [
|
||||||
{ price: payment_schedule.scheduled.plan.stp_price_id }
|
{ price: price[:id] }
|
||||||
]
|
]
|
||||||
}, { api_key: Setting.get('stripe_secret_key') })
|
}, { api_key: Setting.get('stripe_secret_key') })
|
||||||
payment_schedule.update_attributes(stp_subscription_id: stp_subscription.id)
|
payment_schedule.update_attributes(stp_subscription_id: stp_subscription.id)
|
||||||
|
@ -6,6 +6,7 @@ class CreatePaymentScheduleItems < ActiveRecord::Migration[5.2]
|
|||||||
create_table :payment_schedule_items do |t|
|
create_table :payment_schedule_items do |t|
|
||||||
t.integer :amount
|
t.integer :amount
|
||||||
t.datetime :due_date
|
t.datetime :due_date
|
||||||
|
t.jsonb :details, default: '{}'
|
||||||
t.belongs_to :payment_schedule, foreign_key: true
|
t.belongs_to :payment_schedule, foreign_key: true
|
||||||
t.belongs_to :invoice, foreign_key: true
|
t.belongs_to :invoice, foreign_key: true
|
||||||
|
|
||||||
|
@ -1470,6 +1470,7 @@ CREATE TABLE public.payment_schedule_items (
|
|||||||
id bigint NOT NULL,
|
id bigint NOT NULL,
|
||||||
amount integer,
|
amount integer,
|
||||||
due_date timestamp without time zone,
|
due_date timestamp without time zone,
|
||||||
|
details jsonb DEFAULT '"{}"'::jsonb,
|
||||||
payment_schedule_id bigint,
|
payment_schedule_id bigint,
|
||||||
invoice_id bigint,
|
invoice_id bigint,
|
||||||
created_at timestamp without time zone NOT NULL,
|
created_at timestamp without time zone NOT NULL,
|
||||||
|
@ -77,5 +77,6 @@ This is currently not supported, because of some PostgreSQL specific instruction
|
|||||||
- `app/models/project.rb` is using the `pg_search` gem.
|
- `app/models/project.rb` is using the `pg_search` gem.
|
||||||
- `db/migrate/20200622135401_add_pg_search_dmetaphone_support_functions.rb` is using [fuzzystrmatch](http://www.postgresql.org/docs/current/static/fuzzystrmatch.html) module and defines a PL/pgSQL function (`pg_search_dmetaphone()`);
|
- `db/migrate/20200622135401_add_pg_search_dmetaphone_support_functions.rb` is using [fuzzystrmatch](http://www.postgresql.org/docs/current/static/fuzzystrmatch.html) module and defines a PL/pgSQL function (`pg_search_dmetaphone()`);
|
||||||
- `db/migrate/20200623134900_add_search_vector_to_project.rb` is using [tsvector](https://www.postgresql.org/docs/10/datatype-textsearch.html), a PostgreSQL datatype and [GIN (Generalized Inverted Index)](https://www.postgresql.org/docs/9.1/textsearch-indexes.html) a PostgreSQL index type;
|
- `db/migrate/20200623134900_add_search_vector_to_project.rb` is using [tsvector](https://www.postgresql.org/docs/10/datatype-textsearch.html), a PostgreSQL datatype and [GIN (Generalized Inverted Index)](https://www.postgresql.org/docs/9.1/textsearch-indexes.html) a PostgreSQL index type;
|
||||||
- `db/migrate/20200623141305_update_search_vector_of_projects.rb` is defines a PL/pgSQL function (`fill_search_vector_for_project()`) and create an SQL trigger for this function;
|
- `db/migrate/20200623141305_update_search_vector_of_projects.rb` defines a PL/pgSQL function (`fill_search_vector_for_project()`) and create an SQL trigger for this function;
|
||||||
- `db/migrate/20200629123011_update_pg_trgm.rb` is using [ALTER EXTENSION](https://www.postgresql.org/docs/10/sql-alterextension.html);
|
- `db/migrate/20200629123011_update_pg_trgm.rb` is using [ALTER EXTENSION](https://www.postgresql.org/docs/10/sql-alterextension.html);
|
||||||
|
- `db/migrate/20201027101809_create_payment_schedule_items.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user