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
*/
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]);
}
/**
@ -57,7 +57,7 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
{hasEqualDeadlines() && <ul>
<li>
<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 className="schedule-item-date">{t('app.shared.cart.first_debit')}</span>
</li>
@ -65,12 +65,12 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
{!hasEqualDeadlines() && <ul>
<li>
<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>
</li>
<li>
<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>
</li>
</ul>}
@ -81,7 +81,7 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
<li key={String(item.due_date)}>
<span className="schedule-item-date">{formatDate(item.due_date)}</span>
<span> </span>
<span className="schedule-item-price">{formatPrice(item.price)}</span>
<span className="schedule-item-price">{formatPrice(item.amount)}</span>
</li>
))}
</ul>
@ -94,6 +94,7 @@ const PaymentScheduleSummaryWrapper: React.FC<PaymentScheduleSummaryProps> = ({
return (
<Loader>
<PaymentScheduleSummary schedule={schedule} $filter={$filter} />
<div>lorem ipsum</div>
</Loader>
);
}

View File

@ -1,7 +1,12 @@
export interface PaymentScheduleItem {
id: number,
price: number,
amount: number,
due_date: Date
details: {
recurring: number,
adjustment: number,
other_items: number
}
}
export interface PaymentSchedule {

View File

@ -3,6 +3,7 @@
# Coupon is a textual code associated with a discount rate or an amount of discount
class Coupon < ApplicationRecord
has_many :invoices
has_many :payment_schedule
after_commit :create_stripe_coupon, on: [:create]
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'
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'
before_create :add_environment

View File

@ -10,6 +10,7 @@ class InvoicingProfile < ApplicationRecord
has_one :organization, dependent: :destroy
accepts_nested_attributes_for :organization, allow_destroy: false
has_many :invoices, dependent: :destroy
has_many :payment_schedules, dependent: :destroy
has_one :wallet, dependent: :destroy
has_many :wallet_transactions, dependent: :destroy
@ -17,6 +18,7 @@ class InvoicingProfile < ApplicationRecord
has_many :history_values, 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
# 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 :invoicing_profile
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

View File

@ -3,4 +3,5 @@
# Represents a due date and the associated amount for a PaymentSchedule
class PaymentScheduleItem < ApplicationRecord
belongs_to :payment_schedule
belongs_to :invoice
end

View File

@ -144,10 +144,9 @@ class Price < ApplicationRecord
cp = cs.validate(options[:coupon_code], user.id)
total_amount = cs.apply(total_amount, cp)
# == generate PaymentSchedule ()if applicable) ===
# == generate PaymentSchedule (if applicable) ===
schedule = if options[:payment_schedule] && plan.monthly_payment
pss = PaymentScheduleService.new
pss.compute(plan, _amount_no_coupon, cp)
PaymentScheduleService.new.compute(plan, _amount_no_coupon, cp)
else
nil
end

View File

@ -18,6 +18,7 @@ class Reservation < ApplicationRecord
accepts_nested_attributes_for :tickets, allow_destroy: false
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
validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }

View File

@ -7,6 +7,7 @@ class Subscription < ApplicationRecord
belongs_to :plan
belongs_to :statistic_profile
has_one :payment_schedule, as: :scheduled, dependent: :destroy
has_many :invoices, as: :invoiced, dependent: :destroy
has_many :offer_days, dependent: :destroy
@ -53,7 +54,7 @@ class Subscription < ApplicationRecord
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
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

View File

@ -9,6 +9,7 @@ class WalletTransaction < ApplicationRecord
belongs_to :reservation
belongs_to :transactable, polymorphic: true
has_one :invoice
has_one :payment_schedule
validates_inclusion_of :transaction_type, in: %w[credit debit]
validates :invoicing_profile, :wallet, presence: true

View File

@ -28,7 +28,10 @@ class PaymentScheduleService
items = []
(0..deadlines - 1).each do |i|
date = DateTime.current + i.months
details = { recurring: per_month }
amount = if i.zero?
details[:adjustment] = adjustment
details[:other_items] = other_items
per_month + adjustment + other_items
else
per_month
@ -36,13 +39,14 @@ class PaymentScheduleService
items.push PaymentScheduleItem.new(
amount: amount,
due_date: date,
payment_schedule: ps
payment_schedule: ps,
details: details
)
end
{ payment_schedule: ps, items: items }
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)
ps = schedule[:payment_schedule]
items = schedule[:items]
@ -50,11 +54,13 @@ class PaymentScheduleService
ps.scheduled = subscription
ps.payment_method = payment_method
ps.operator_profile_id = operator
ps.save!
# TODO, fields: reference, wallet_amount, wallet_transaction_id, footprint, environment, invoicing_profile
items.each do |item|
item.payment_schedule = ps
item.save!
end
StripeWorker.perform_async(:create_stripe_subscription, ps.id)
StripeWorker.perform_async(:create_stripe_subscription, ps.id, reservation&.reservable&.stp_product_id)
end
end

View File

@ -13,7 +13,7 @@ end
if @amount[:schedule]
json.schedule do
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
end
end

View File

@ -68,30 +68,54 @@ class StripeWorker
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)
first_item = payment_schedule.ordered_items.first
second_item = payment_schedule.ordered_items[1]
items = []
first_invoice_items.each do |fii|
# TODO, fill this prices with real data
price = Stripe::Price.create({
unit_amount: 2000,
currency: 'eur',
recurring: { interval: 'month' },
product_data: {
name: 'lorem ipsum'
}
},
{ api_key: Setting.get('stripe_secret_key') })
items.push(price: price[:id])
if first_item.amount != second_item.amount
if first_item.details[:adjustment]
# adjustment: when dividing the price of the plan / months, sometimes it forces us to round the amount per month.
# The difference is invoiced here
p1 = Stripe::Price.create({
unit_amount: first_item.details[:adjustment],
currency: Setting.get('stripe_currency'),
product: payment_schedule.scheduled.plan.stp_product_id,
nickname: "Price adjustment payment schedule #{payment_schedule_id}"
}, { api_key: Setting.get('stripe_secret_key') })
items.push(price: p1[: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
# 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({
customer: payment_schedule.invoicing_profile.user.stp_customer_id,
cancel_at: payment_schedule.scheduled.expiration_date,
promotion_code: payment_schedule.coupon&.code,
add_invoice_items: items,
items: [
{ price: payment_schedule.scheduled.plan.stp_price_id }
{ price: price[:id] }
]
}, { api_key: Setting.get('stripe_secret_key') })
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|
t.integer :amount
t.datetime :due_date
t.jsonb :details, default: '{}'
t.belongs_to :payment_schedule, 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,
amount integer,
due_date timestamp without time zone,
details jsonb DEFAULT '"{}"'::jsonb,
payment_schedule_id bigint,
invoice_id bigint,
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.
- `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/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/20201027101809_create_payment_schedule_items.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html);