1
0
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:
Sylvain 2020-11-12 16:44:55 +01:00
parent ed5b90cbdc
commit b5504d2342
17 changed files with 87 additions and 29 deletions

View File

@ -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>
); );
} }

View File

@ -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 {

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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);