-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
wip: paid subscriptions system and articles #467
base: master
Are you sure you want to change the base?
Conversation
Co-authored-by: DioFun <DioFun@users.noreply.github.com> Co-authored-by: Nymous <thomas.gaudin@centraliens-lille.org>
todo: fix tests
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #467 +/- ##
==========================================
Coverage 100.00% 100.00%
==========================================
Files 26 48 +22
Lines 337 663 +326
Branches 35 66 +31
==========================================
+ Hits 337 663 +326 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same, just posting what I wrote for now (also you pushed after I started to review so some comments might be outdated ^^')
def destroy | ||
@article = Article.find(params[:id]) | ||
authorize! :destroy, @article | ||
@article.soft_delete unless @article.destroy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a comment to explain why we do this.
Alternative way to write it, not really convinced by it but at least it reads from left to right (and there isn't a side-effect in the conditional):
@article.soft_delete unless @article.destroy | |
@article.destroy or @article.soft_delete |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment still stands
module Admin | ||
class DashboardController < ApplicationController | ||
def index | ||
authorize! :manage, :all |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a temporary permission and we will check more granularly, or is the admin dashboard only intended for superadmins?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is temporary permission for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a todo if it's temporary
authorize! :manage, :all | |
authorize! :manage, :all # TODO: use finer grained permissions |
def destroy | ||
@payment_method = PaymentMethod.find(params[:id]) | ||
authorize! :destroy, @payment_method | ||
@payment_method.soft_delete unless @payment_method.destroy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing here, maybe comment, maybe write it differently
@payment_method.soft_delete unless @payment_method.destroy | |
@payment_method.destroy or @payment_method.soft_delete |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bump comment
def destroy | ||
@subscription_offer = SubscriptionOffer.find(params[:id]) | ||
authorize! :destroy, @subscription_offer | ||
@subscription_offer.soft_delete unless @subscription_offer.destroy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same, comment or
@subscription_offer.soft_delete unless @subscription_offer.destroy | |
@subscription_offer.destroy or @subscription_offer.soft_delete |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bump comment
app/controllers/sales_controller.rb
Outdated
params.require(:sale).permit(:duration, :payment_method_id, articles_sales_attributes: [:article_id, :quantity]) | ||
end | ||
|
||
def reformated_params |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The params methods could probably do with some comments and vertical space to separate blocks ^^'
app/controllers/sales_controller.rb
Outdated
# rubocop:disable Metrics/AbcSize | ||
def create | ||
@sale = @owner.sales_as_client.new(reformated_params) | ||
@sale.update_total_price |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooooh, it's dangerous to have to think to do this before saving, can this be moved to a before_save
or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Has been moved but not sure if it's enough
validates :name, presence: true, allow_blank: false | ||
validates :price, presence: true, allow_blank: false, | ||
numericality: { greater_than_or_equal_to: 0, only_integer: true, message: 'Must be a positive | ||
number. Maximum 2 numbers after comma' } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure the error message is up-to-date with the fact that we store it as cents.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the UI, the user will be ask to enter a decimal number and then it'll be multiply by 100
…ash generation for sale
unless @sale.generate(duration: params[:sale][:duration], seller: current_user) | ||
return redirect_to :new_user_sale, user: @user, status: :unprocessable_entity | ||
end | ||
return redirect_to :new_user_sale, user: @user, status: :unprocessable_entity if @sale.empty? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what happens if we try to call generate
on an empty sale? Before generating it, should we check if the sale isn't empty?
// newArticle.getElementById("sale_article_id_new").id = `sale_article_id_${this.nextId}` | ||
// newArticle.getElementById("sale_quantity_new").id = `sale_quantity_${this.nextId}` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this be removed?
def compute_total_price | ||
total = 0 | ||
articles_sales.each do |rec| | ||
total += rec.quantity * Article.find(rec.article_id).price | ||
end | ||
sales_subscription_offers.each do |rec| | ||
total += rec.quantity * SubscriptionOffer.find(rec.subscription_offer.id).price | ||
end | ||
total | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't care about that at our scale because we are talking about one or two articles, but I think here we would make a request to the database per article/subscription. That can be a problem once you get to dozens of articles.
We don't need to change anything for our case though, it's simpler like that
def generate_sales_subscription_offers(duration) | ||
subscription_offers = SubscriptionOffer.order(duration: :desc) | ||
if subscription_offers.empty? | ||
errors.add(:base, 'There are no subscription offers registered!') | ||
return false | ||
end | ||
subscription_offers.each do |offer| | ||
break if duration.zero? | ||
|
||
quantity = duration / offer.duration | ||
if quantity.positive? | ||
sales_subscription_offers.new(subscription_offer_id: offer.id, quantity: quantity) | ||
duration -= quantity * offer.duration | ||
end | ||
end | ||
return true if duration.zero? | ||
|
||
errors.add(:base, 'Subscription offers are not exhaustive!') | ||
false | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit lost trying to understand what this function is doing. What does the duration parameter represent? Why do we leave the loop once the duration is zero? Why do we return false when there is an error, should we instead throw if it's unexpected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function aims to generate the subiscription_offer for a given duration, duration is the number of months the subscription last. Example : an adherent want to pay for 13 months and we have two different offer 12 months is 50€ and 1 month is 5€ then it will create a sales_subscription_offers of 12 months and of 1 months. So we leave the loop when reach 0 because we do not need to continue as we already split the sale into the different offers we have. Lastly, we return false, because it's quite obscure the error process and the throw in my case i'd like just to send a message to the user not crash the website.
# validate :cannot_change_after_cancelled, on: :update | ||
|
||
def cancel! | ||
self.cancelled_at = Time.current | ||
save! | ||
def user | ||
sale.client | ||
end | ||
|
||
private | ||
# def cancel! | ||
# self.cancelled_at = Time.current | ||
# save! | ||
# end | ||
|
||
def cannot_change_after_cancelled | ||
return if cancelled_at_was.nil? | ||
# private | ||
|
||
errors.add(:cancelled_at, 'Subscription has already been cancelled') | ||
end | ||
# def cannot_change_after_cancelled | ||
# return if cancelled_at_was.nil? | ||
# | ||
# errors.add(:cancelled_at, 'Subscription has already been cancelled') | ||
# end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we delete the comments if it's not used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just commented it out for now because we would surely use it again when we'll make the refund module.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will need to add the font license if we use it (available in the font archive), probably as a app/assets/fronts/dejavu-license.txt
.
def destroy | ||
@article = Article.find(params[:id]) | ||
authorize! :destroy, @article | ||
@article.soft_delete unless @article.destroy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment still stands
module Admin | ||
class DashboardController < ApplicationController | ||
def index | ||
authorize! :manage, :all |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a todo if it's temporary
authorize! :manage, :all | |
authorize! :manage, :all # TODO: use finer grained permissions |
def destroy | ||
@payment_method = PaymentMethod.find(params[:id]) | ||
authorize! :destroy, @payment_method | ||
@payment_method.soft_delete unless @payment_method.destroy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bump comment
def destroy | ||
@subscription_offer = SubscriptionOffer.find(params[:id]) | ||
authorize! :destroy, @subscription_offer | ||
@subscription_offer.soft_delete unless @subscription_offer.destroy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bump comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With all the validation logic that I moved to the Sale model, this test will probably need more cases:
- test if sale is empty
- test if sale has duplicate articles
assert_not_predicate @subscription_offer, :valid? | ||
end | ||
|
||
test 'price should be positive' do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also test for strictly positive? (a subscription offer that costs 0 is weird)
end | ||
|
||
test 'duration should be positive' do | ||
@subscription_offer.duration = -5 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, test for strictly positive as well
test 'offer should be destroyed if no sales' do | ||
@subscription_offer.sales.destroy_all | ||
@subscription_offer.refunds.destroy_all | ||
assert_difference 'SubscriptionOffer.unscoped.count', -1 do | ||
@subscription_offer.destroy | ||
end | ||
end | ||
|
||
test 'offer should be destroyable' do | ||
@subscription_offer.sales.destroy_all | ||
@subscription_offer.refunds.destroy_all | ||
assert_predicate @subscription_offer, :destroy | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, should this be merged?
test 'offer should be destroyed if no sales' do | |
@subscription_offer.sales.destroy_all | |
@subscription_offer.refunds.destroy_all | |
assert_difference 'SubscriptionOffer.unscoped.count', -1 do | |
@subscription_offer.destroy | |
end | |
end | |
test 'offer should be destroyable' do | |
@subscription_offer.sales.destroy_all | |
@subscription_offer.refunds.destroy_all | |
assert_predicate @subscription_offer, :destroy | |
end | |
test 'offer should be destroyed if no sales' do | |
@subscription_offer.sales.destroy_all | |
@subscription_offer.refunds.destroy_all | |
assert_difference 'SubscriptionOffer.unscoped.count', -1 do | |
assert_predicate @subscription_offer, :destroy | |
end | |
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow very nice tests on a hard-to-test feature!
Le but est d'avoir un suivi et des correctifs aux fur et à mesure pour le moment quant au système d'abonnement payant.
Développement de la gestion des articles et abonnements payants.
La gestion des remboursements sera effectuée dans une autre PR
links to #455