From b5504d2342251e089913d1afd50af5fa5fd18306 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Thu, 12 Nov 2020 16:44:55 +0100 Subject: [PATCH] create stripe subscription with all data --- .../components/payment-schedule-summary.tsx | 11 ++-- .../src/javascript/models/payment-schedule.ts | 7 ++- app/models/coupon.rb | 1 + app/models/invoice.rb | 1 + app/models/invoicing_profile.rb | 2 + app/models/payment_schedule.rb | 12 +++++ app/models/payment_schedule_item.rb | 1 + app/models/price.rb | 5 +- app/models/reservation.rb | 1 + app/models/subscription.rb | 3 +- app/models/wallet_transaction.rb | 1 + app/services/payment_schedule_service.rb | 12 +++-- app/views/api/prices/compute.json.jbuilder | 2 +- app/workers/stripe_worker.rb | 52 ++++++++++++++----- ...027101809_create_payment_schedule_items.rb | 1 + db/structure.sql | 1 + doc/postgresql_readme.md | 3 +- 17 files changed, 87 insertions(+), 29 deletions(-) diff --git a/app/frontend/src/javascript/components/payment-schedule-summary.tsx b/app/frontend/src/javascript/components/payment-schedule-summary.tsx index c8e48b97c..2d2875d7e 100644 --- a/app/frontend/src/javascript/components/payment-schedule-summary.tsx +++ b/app/frontend/src/javascript/components/payment-schedule-summary.tsx @@ -40,7 +40,7 @@ const PaymentScheduleSummary: React.FC = ({ 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 = ({ schedul {hasEqualDeadlines() &&
  • - {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) })} {t('app.shared.cart.first_debit')}
  • @@ -65,12 +65,12 @@ const PaymentScheduleSummary: React.FC = ({ schedul {!hasEqualDeadlines() &&
    • {t('app.shared.cart.monthly_payment_NUMBER', { NUMBER: 1 })} - {formatPrice(schedule.items[0].price)} + {formatPrice(schedule.items[0].amount)} {t('app.shared.cart.debit')}
    • - {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) })}
    } @@ -81,7 +81,7 @@ const PaymentScheduleSummary: React.FC = ({ schedul
  • {formatDate(item.due_date)} - {formatPrice(item.price)} + {formatPrice(item.amount)}
  • ))}
@@ -94,6 +94,7 @@ const PaymentScheduleSummaryWrapper: React.FC = ({ return ( +
lorem ipsum
); } diff --git a/app/frontend/src/javascript/models/payment-schedule.ts b/app/frontend/src/javascript/models/payment-schedule.ts index 5afc59ee3..e9237cba8 100644 --- a/app/frontend/src/javascript/models/payment-schedule.ts +++ b/app/frontend/src/javascript/models/payment-schedule.ts @@ -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 { diff --git a/app/models/coupon.rb b/app/models/coupon.rb index 54cae97a7..d335c56f3 100644 --- a/app/models/coupon.rb +++ b/app/models/coupon.rb @@ -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] diff --git a/app/models/invoice.rb b/app/models/invoice.rb index efe5cb030..08b0f1772 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -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 diff --git a/app/models/invoicing_profile.rb b/app/models/invoicing_profile.rb index 68d890b80..ed2b21fe0 100644 --- a/app/models/invoicing_profile.rb +++ b/app/models/invoicing_profile.rb @@ -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 diff --git a/app/models/payment_schedule.rb b/app/models/payment_schedule.rb index 3cf06a745..3e039e9b5 100644 --- a/app/models/payment_schedule.rb +++ b/app/models/payment_schedule.rb @@ -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 diff --git a/app/models/payment_schedule_item.rb b/app/models/payment_schedule_item.rb index 3c2a3215a..54733021d 100644 --- a/app/models/payment_schedule_item.rb +++ b/app/models/payment_schedule_item.rb @@ -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 diff --git a/app/models/price.rb b/app/models/price.rb index c6b993a8e..e3a3e7c69 100644 --- a/app/models/price.rb +++ b/app/models/price.rb @@ -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 diff --git a/app/models/reservation.rb b/app/models/reservation.rb index eec8cf95a..7c15a568b 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -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) } diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 2f9a517d6..e2fed42be 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -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 diff --git a/app/models/wallet_transaction.rb b/app/models/wallet_transaction.rb index ba071b33f..a00d9c401 100644 --- a/app/models/wallet_transaction.rb +++ b/app/models/wallet_transaction.rb @@ -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 diff --git a/app/services/payment_schedule_service.rb b/app/services/payment_schedule_service.rb index 3cfdaa0a5..ce5567812 100644 --- a/app/services/payment_schedule_service.rb +++ b/app/services/payment_schedule_service.rb @@ -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 diff --git a/app/views/api/prices/compute.json.jbuilder b/app/views/api/prices/compute.json.jbuilder index 2f599976c..6dfd2318b 100644 --- a/app/views/api/prices/compute.json.jbuilder +++ b/app/views/api/prices/compute.json.jbuilder @@ -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 diff --git a/app/workers/stripe_worker.rb b/app/workers/stripe_worker.rb index 50d984902..0b74c53be 100644 --- a/app/workers/stripe_worker.rb +++ b/app/workers/stripe_worker.rb @@ -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) diff --git a/db/migrate/20201027101809_create_payment_schedule_items.rb b/db/migrate/20201027101809_create_payment_schedule_items.rb index e32d7a85a..150e22d69 100644 --- a/db/migrate/20201027101809_create_payment_schedule_items.rb +++ b/db/migrate/20201027101809_create_payment_schedule_items.rb @@ -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 diff --git a/db/structure.sql b/db/structure.sql index a98bbbbae..3436e6be6 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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, diff --git a/doc/postgresql_readme.md b/doc/postgresql_readme.md index 62bb1aaaf..6ae5a2943 100644 --- a/doc/postgresql_readme.md +++ b/doc/postgresql_readme.md @@ -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);