diff --git a/Gemfile b/Gemfile index a57bf50..120ac9d 100644 --- a/Gemfile +++ b/Gemfile @@ -11,9 +11,11 @@ gem "puma", "~> 5.0" gem "rails", "~> 7.0.3" gem "dotenv-rails" gem "pundit" +gem "ransack" group :development, :test do gem "debug", platforms: %i[ mri mingw x64_mingw ] + gem "timecop" end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 83e438f..8f490b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -159,10 +159,15 @@ GEM thor (~> 1.0) zeitwerk (~> 2.5) rake (13.0.6) + ransack (3.2.1) + activerecord (>= 6.1.5) + activesupport (>= 6.1.5) + i18n reline (0.3.1) io-console (~> 0.5) strscan (3.0.3) thor (1.2.1) + timecop (0.9.5) timeout (0.3.0) tzinfo (2.0.4) concurrent-ruby (~> 1.0) @@ -189,6 +194,8 @@ DEPENDENCIES puma (~> 5.0) pundit rails (~> 7.0.3) + ransack + timecop web-console RUBY VERSION diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index dcd7273..b735090 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1 +1,7 @@ /* Application styles */ + +table { border: 1px solid black; } + +td, th { padding: 10px; } + +.new_contribution_form { max-width: 600px; } diff --git a/app/controllers/contributions_controller.rb b/app/controllers/contributions_controller.rb new file mode 100644 index 0000000..08820f5 --- /dev/null +++ b/app/controllers/contributions_controller.rb @@ -0,0 +1,54 @@ +class ContributionsController < ApplicationController + before_action :set_member, only: %i[ new create ] + before_action :set_contribution, only: %i[ edit ] + + # GET /members/new + def new + @contribution = Contribution.new + end + + # GET /members/1/edit + def edit + end + + # POST /members + def create + @contribution = @member.contributions.build(contribution_params) + + Contribution.transaction do + if @contribution.save + @member.handle_new_contribution(@contribution, params.dig(:contribution, :overriden_expires_on)) + @member.reset_status! + + redirect_to @member, notice: "Contribution was successfully created." + else + render :new, status: :unprocessable_entity + end + end + end + + ## PATCH/PUT /members/1 + #def update + # if @member.update(member_params) + # redirect_to @member, notice: "Member was successfully updated." + # else + # render :edit, status: :unprocessable_entity + # end + #end + + private + # Use callbacks to share common setup or constraints between actions. + def set_member + @member = Member.find(params[:member_id]) + end + + def set_contribution + @contribution = Contribution.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def contribution_params + params.fetch(:contribution, {}).permit(:eurocents, :payment_method, :payment_on, :payment_reference) + end +end + diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb new file mode 100644 index 0000000..11bccae --- /dev/null +++ b/app/controllers/members_controller.rb @@ -0,0 +1,72 @@ +class MembersController < ApplicationController + before_action :set_member, only: %i[ show edit update destroy ] + helper_method :sort_params + + # GET /members + def index + params.delete(:status) if params[:status] == 'any' + params.delete(:category) if params[:category] == 'any' + + @members = Member.all.order(sort_params.merge(number: :asc)) + @members = @members.ransack(display_name_or_email_or_identification_number_i_cont: params[:q], status_cont: params[:status], category_cont: params[:category]).result + end + + # GET /members/1 + def show + end + + # GET /members/new + def new + @member = Member.new + end + + # GET /members/1/edit + def edit + end + + # POST /members + def create + @member = Member.new(member_params) + + if @member.save + @member.reset_status! + redirect_to @member, notice: "Member was successfully created." + else + render :new, status: :unprocessable_entity + end + end + + # PATCH/PUT /members/1 + def update + if @member.update(member_params) + @member.reload.reset_status! + redirect_to @member, notice: "Member was successfully updated." + else + render :edit, status: :unprocessable_entity + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_member + @member = Member.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def member_params + params.fetch(:member, {}).permit(:display_name, :email, :identification_number, :category, :address, :joined_on, :expires_on) + end + + def sort_params + field, direction = params.fetch(:sort, "").split(".") + + directions = %w[ asc desc ] + fields = %w[ number expires_on joined_on email status display_name ] + + if directions.include?(direction) && fields.include?(field) + { field => direction } + else + { number: :asc } + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be79..5da3d9b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,5 @@ module ApplicationHelper + def member_status(status) + t("members.status.#{status}") + end end diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb new file mode 100644 index 0000000..eaf9de3 --- /dev/null +++ b/app/helpers/members_helper.rb @@ -0,0 +1,24 @@ +module MembersHelper + def link_to_current_with_sort text, default_sort + current_sort = stringify(sort_params) + + pp [default_sort, current_sort] + + if default_sort == current_sort + link_to text, members_path(sort: invert_sort_order(current_sort)) + else + link_to text, members_path(sort: default_sort) + end + + end + + private + + def stringify(sort) + "#{sort.keys.first.to_s}.#{sort.values.first.to_s}" + end + + def invert_sort_order(sort) + sort.sub(/\.(asc|desc)$/) { |x| x == '.asc' ? '.desc' : '.asc' } + end +end diff --git a/app/lib/if_then_pay.rb b/app/lib/if_then_pay.rb new file mode 100644 index 0000000..4f8dd42 --- /dev/null +++ b/app/lib/if_then_pay.rb @@ -0,0 +1,19 @@ +require 'net/http' + +module IfThenPay + def self.generate_gateway_link(id:, amount:, description:) + response = Net::HTTP.post( + URI("https://ifthenpay.com/api/gateway/paybylink/#{ENV['IFTHENPAY_KEY']}"), + JSON.generate({ + id: id, + amount: amount.to_s, + description: description.to_s, + "lang": "pt", + "expiredate": "", + "accounts": ENV['IFTHENPAY_ACCOUNTS'], + }) + ) + + JSON.parse(response.body) + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 3c34c81..8e855f5 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,4 @@ class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" + default from: email_address_with_name(ENV['SMTP_FROM_ADDRESS'], ENV['SMTP_FROM_NAME']) layout "mailer" end diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb new file mode 100644 index 0000000..f39ae39 --- /dev/null +++ b/app/mailers/notification_mailer.rb @@ -0,0 +1,68 @@ +class NotificationMailer < ApplicationMailer + + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.notification_mailer.expiration_in_60d.subject + # + def expiration_in_60d + @notification = params[:notification] + + mail to: params[:notification].member.email + end + + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.notification_mailer.expiration_in_30d.subject + # + def expiration_in_30d + @notification = params[:notification] + + mail to: params[:notification].member.email + end + + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.notification_mailer.expired.subject + # + def expired + @notification = params[:notification] + + mail to: params[:notification].member.email + end + + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.notification_mailer.expired_30d_ago.subject + # + def expired_30d_ago + @notification = params[:notification] + + mail to: params[:notification].member.email + end + + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.notification_mailer.expired_60d_ago.subject + # + def expired_60d_ago + @notification = params[:notification] + + mail to: params[:notification].member.email + end + + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.notification_mailer.cancelled.subject + # + def cancelled + @notification = params[:notification] + + mail to: params[:notification].member.email + end +end diff --git a/app/models/contribution.rb b/app/models/contribution.rb new file mode 100644 index 0000000..8596bab --- /dev/null +++ b/app/models/contribution.rb @@ -0,0 +1,3 @@ +class Contribution < ApplicationRecord + belongs_to :member +end diff --git a/app/models/member.rb b/app/models/member.rb new file mode 100644 index 0000000..a036845 --- /dev/null +++ b/app/models/member.rb @@ -0,0 +1,88 @@ +class Member < ApplicationRecord + has_many :contributions + has_many :notifications + + def cancelled_on + expires_on + 90.days + end + + def reset_status! + update(status: expected_status) + end + + def expected_status + if joined_on.nil? + :pending + elsif (joined_on + 6.months).future? + :passive + elsif expires_on.future? + :active + elsif cancelled_on.future? + :expired + else + :cancelled + end + end + + def handle_new_contribution(contribution, overriden_expires_on) + if joined_on.nil? + self.joined_on = contribution.payment_on + self.expires_on = overriden_expires_on.presence || (joined_on + 1.year) + else + self.expires_on = overriden_expires_on.presence || expires_on + 1.year + end + + save! + end + + def regenerate_notifications + notifications.where(status: 'scheduled').delete_all + + return if expires_on.nil? + + [ + { to_be_sent_on: expires_on - 90.days, template: "expiration_in_60d" }, + { to_be_sent_on: expires_on - 30.days, template: "expiration_in_30d" }, + { to_be_sent_on: expires_on + 0.days, template: "expired" }, + { to_be_sent_on: expires_on + 30.days, template: "expired_30d_ago" }, + { to_be_sent_on: expires_on + 60.days, template: "expired_60d_ago" }, + { to_be_sent_on: expires_on + 90.days, template: "cancelled" }, + ].reject { |n| n[:to_be_sent_on].past? }.each do |n| + notifications.create(n.merge(status: "scheduled")) + end + end + + def generate_missing_ifthenpay_links! + self.regular_ifthenpay_link = IfThenPay.generate_gateway_link( + id: number, + amount: "30.00", + description: "Quotas ANSOL", + ) unless self.regular_ifthenpay_link.present? + + self.reduced_ifthenpay_link = IfThenPay.generate_gateway_link( + id: number, + amount: "6.00", + description: "Quotas ANSOL", + ) unless self.reduced_ifthenpay_link.present? + + save! + end + + def self.reset_all_status! + Member.all.each do |member| + member.reset_status! + end + end + + def self.generate_all_missing_ifthenpay_links! + Member.all.each do |member| + member.generate_missing_ifthenpay_links! + end + end + + def self.regenerate_all_notifications + Member.all.each do |member| + member.regenerate_notifications + end + end +end diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 0000000..4e2f651 --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,20 @@ +class Notification < ApplicationRecord + belongs_to :member + + scope :scheduled_for_today, ->() { where(status: 'scheduled', to_be_sent_on: Date.today) } + + def self.send_scheduled_for_today + scheduled_for_today.each do |n| + n.deliver! + end + end + + def deliver! + # actually send the email. + NotificationMailer.with(notification: self).send(template).deliver_now! + + update(status: 'sent', sent_at: Time.current) + rescue + # TODO: do something about failures + end +end diff --git a/app/views/contributions/edit.html.erb b/app/views/contributions/edit.html.erb new file mode 100644 index 0000000..91b1ebf --- /dev/null +++ b/app/views/contributions/edit.html.erb @@ -0,0 +1,22 @@ +editando contriboot + +<%= form_with(model: @contribution) do |form| %> + + + + + + + + + + + + + + + + + +
Amount€<%= @contribution.eurocents %>
Payment date€<%= @contribution.payment_on %>
Payment method<%= @contribution.payment_method %>
Payment reference<%= @contribution.payment_reference %>
+<% end %> diff --git a/app/views/contributions/new.html.erb b/app/views/contributions/new.html.erb new file mode 100644 index 0000000..4eb3f92 --- /dev/null +++ b/app/views/contributions/new.html.erb @@ -0,0 +1,48 @@ +

Registering contribution for <%= @member.display_name %>

+ + + + + +
Member number<%= @member.number %>
Joined on<%= @member.joined_on %>
Expires on<%= @member.expires_on %>
+ + +<%= form_with(model: [@member, @contribution]) do |form| %> + + + + + + + + + + + + + + + + + + + + + + + + +
<%= form.number_field :eurocents %>
<%= form.date_field :payment_on %>
<%= form.select :payment_method, %w[iban mbway multibanco] %>
<%= form.text_field :payment_reference %>
+ Adding a contribution will automatically bump the membership expiration + date by one year. If it's the first contribution for this member, the + join date will be set to the payment date and the expiration date one + year after that. You can override the expiration date by setting a date + below: +
+ <%= form.date_field :overriden_expires_on %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 5f22252..ae94ca1 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,6 +11,10 @@ + + <%= yield %> diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index cbd34d2..a6c3429 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -8,6 +8,12 @@ - <%= yield %> +
+
+ ANSOL +
+ + <%= yield %> +
diff --git a/app/views/members/_form.html.erb b/app/views/members/_form.html.erb new file mode 100644 index 0000000..e69de29 diff --git a/app/views/members/_member.html.erb b/app/views/members/_member.html.erb new file mode 100644 index 0000000..88d77a0 --- /dev/null +++ b/app/views/members/_member.html.erb @@ -0,0 +1,2 @@ +
+
diff --git a/app/views/members/edit.html.erb b/app/views/members/edit.html.erb new file mode 100644 index 0000000..c850d4c --- /dev/null +++ b/app/views/members/edit.html.erb @@ -0,0 +1,59 @@ +

<%= t('members.edit.title') %>

+ +<%= form_with(model: @member) do |form| %> + <% if @member.errors.any? %> +
+

<%= pluralize(@member.errors.count, "error") %> prohibited this member from being saved:

+ + +
+ <% end %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
<%= form.text_field :display_name, required: true %>
<%= form.email_field :email, required: true %>
<%= form.select :category, %w{student retired unemployed employed} %>
<%= form.text_field :identification_number %>
<%= form.text_area :address %>
<%= t('members.edit.edit_dates_warning') %>
<%= form.date_field :joined_on %>
<%= form.date_field :expires_on %>
+ +
+ <%= form.submit %> +
+<% end %> + +
+ +
+ <%= link_to t('members.edit.actions.back_to_show'), @member %> +
diff --git a/app/views/members/index.html.erb b/app/views/members/index.html.erb new file mode 100644 index 0000000..71dc6c5 --- /dev/null +++ b/app/views/members/index.html.erb @@ -0,0 +1,44 @@ +

<%= notice %>

+ +

<%= t 'members.index.title' %>

+ +<%= link_to t('members.index.actions.new'), new_member_path %> + +<%= form_with url: members_path, method: :get do |form| %> + <%= form.text_field :q %> + <%= form.select :status, %w[ any active passive pending expired cancelled ], selected: params[:status] %> + <%= form.select :category, %w[ any student employed unemployed retired ], selected: params[:category], multiple: true %> + <%= form.submit 'Search', name: '' %> + <% if params[:q].present? || params[:status].present? || params[:category].present? %> + <%= link_to t('members.index.actions.clear_search'), members_path %> + <% end %> +<% end %> + + + + + + + + + + + + + + <% @members.each do |member| %> + + + + + + + + + + <% end %> +
<%= link_to_current_with_sort t('members.attributes.number'), 'number.asc' %><%= link_to_current_with_sort t('members.attributes.status'), 'status.asc' %><%= link_to_current_with_sort t('members.attributes.email'), 'email.asc' %><%= link_to_current_with_sort t('members.attributes.display_name'), 'display_name.asc' %><%= link_to_current_with_sort t('members.attributes.joined_on'), 'joined_on.asc' %><%= link_to_current_with_sort t('members.attributes.expires_on'), 'expires_on.asc' %><%= t('members.index.actions.title') %>
<%= member.number %><%= member_status(member.status) %><%= member.email %><%= member.display_name %><%= member.joined_on %><%= member.expires_on %> + <%= link_to t('members.index.actions.show'), member %> | + <%= link_to t('members.index.actions.edit'), edit_member_path(member) %> | + <%= link_to t('members.index.actions.new_contribution'), new_member_contribution_path(member) %> +
diff --git a/app/views/members/new.html.erb b/app/views/members/new.html.erb new file mode 100644 index 0000000..80c2601 --- /dev/null +++ b/app/views/members/new.html.erb @@ -0,0 +1,27 @@ +

<%= t('members.new.title') %>

+ +<%= form_with(model: @member) do |form| %> + <% if @member.errors.any? %> +
+

<%= pluralize(@member.errors.count, "error") %> prohibited this member from being saved:

+ + +
+ <% end %> + + + + + + + +
<%= form.text_field :display_name, required: true %>
<%= form.email_field :email, required: true %>
<%= form.select :category, %w{student retired unemployed employed} %>
<%= form.text_field :identification_number %>
<%= form.text_area :address %>
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/members/show.html.erb b/app/views/members/show.html.erb new file mode 100644 index 0000000..7d45f55 --- /dev/null +++ b/app/views/members/show.html.erb @@ -0,0 +1,40 @@ +

<%= notice %>

+ +

<%= t('members.show.title') %>

+ + + + + + + + + + +
<%= t('members.attributes.display_name') %><%= @member.display_name %>
<%= t('members.attributes.email') %><%= @member.email %>
<%= t('members.attributes.category') %><%= @member.category %>
<%= t('members.attributes.identification_number') %><%= @member.identification_number %>
<%= t('members.attributes.address') %><%= simple_format @member.address %>
<%= t('members.attributes.joined_on') %><%= @member.joined_on %>
<%= t('members.attributes.expires_on') %><%= @member.expires_on %>
<%= t('members.attributes.status') %><%= @member.status %>
+ +
+ <%= link_to t('members.show.actions.edit'), edit_member_path(@member) %> +
+ +

<%= t('members.show.contribution_history') %>

+ + + + + + + + + <% @member.contributions.each do |contribution| %> + + + + + + + + <% end %> +
Payment datePayment methodPayment referenceAmount
<%= contribution.payment_on %><%= contribution.payment_method %><%= contribution.payment_reference %>€<%= contribution.eurocents %> + <%= link_to t('members.show.actions.edit_contribution'), edit_contribution_path(contribution) %> +
diff --git a/app/views/notification_mailer/_payment.html.erb b/app/views/notification_mailer/_payment.html.erb new file mode 100644 index 0000000..531c9e0 --- /dev/null +++ b/app/views/notification_mailer/_payment.html.erb @@ -0,0 +1,22 @@ +

+ Aceitamos pagamento via transferência bancária, referência multibanco ou + MBWAY: +

+ + + +

+ Caso queiras usufruir da quota reduzida de 6.00€ para estudantes, + desempregados e reformados, pedimos que nos envies um comprovativo desse + estatuto e faças o pagamento por transferência bancária. +

+ +

+ Se optares pelo método de transferência bancária, pedimos que envies o + comprovativo de transferência em resposta a este email ou para o endereço + direccao@ansol.org. +

diff --git a/app/views/notification_mailer/_payment.text.erb b/app/views/notification_mailer/_payment.text.erb new file mode 100644 index 0000000..2cfd09d --- /dev/null +++ b/app/views/notification_mailer/_payment.text.erb @@ -0,0 +1,14 @@ +Aceitamos pagamento via transferência bancária, referência multibanco ou +MBWAY: + +* Valor: 30.00€ +* Transferência bancária: PT50 0035 2178 00027478430 14 +* Multibanco ou MBWAY: <%= @link %> + +Caso queiras usufruir da quota reduzida de 6.00€ para estudantes, +desempregados e reformados, pedimos que nos envies um comprovativo desse +estatuto e faças o pagamento por transferência bancária. + +Se optares pelo método de transferência bancária, pedimos que envies o +comprovativo de transferência em resposta a este email ou para o endereço +direccao@ansol.org. diff --git a/app/views/notification_mailer/cancelled.html.erb b/app/views/notification_mailer/cancelled.html.erb new file mode 100644 index 0000000..c8bd29d --- /dev/null +++ b/app/views/notification_mailer/cancelled.html.erb @@ -0,0 +1,30 @@ +

+ <%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %> +

+ +

+ Como não recebemos o pagamento anual das quotas da ANSOL, a tua inscrição foi + cancelada. +

+ +

+ Vamos revogar em breve os teus acessos à infraestrutura da associação + exclusiva para membros (nextcloud, mailing list, sala de Matrix, etc). +

+ +

+ Esperamos poder voltar a merecer o teu apoio no futuro. Podes reinscrever-te + a qualquer altura através do formulário disponível em + https://ansol.org/inscricao. +

+ +

+ Caso consideres que estás a receber esta mensagem indevidamente, contacta-nos + através do endereço direccao@ansol.org para resolvermos a situação o mais + rápido possível. +

+ +

+ Saudações livres,
+ Direcção da ANSOL +

diff --git a/app/views/notification_mailer/cancelled.text.erb b/app/views/notification_mailer/cancelled.text.erb new file mode 100644 index 0000000..6d672b4 --- /dev/null +++ b/app/views/notification_mailer/cancelled.text.erb @@ -0,0 +1,17 @@ +<%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %> + +Como não recebemos o pagamento anual das quotas da ANSOL, a tua inscrição foi +cancelada. + +Vamos revogar em breve os teus acessos à infraestrutura da associação exclusiva +para membros (nextcloud, mailing list, sala de Matrix, etc). + +Esperamos poder voltar a merecer o teu apoio no futuro. Podes reinscrever-te a +qualquer altura através do formulário disponível em https://ansol.org/inscricao + +Caso consideres que estás a receber esta mensagem indevidamente, contacta-nos +através do endereço direccao@ansol.org para resolvermos a situação o mais +rápido possível. + +Saudações livres, +Direcção da ANSOL diff --git a/app/views/notification_mailer/expiration_in_30d.html.erb b/app/views/notification_mailer/expiration_in_30d.html.erb new file mode 100644 index 0000000..64186d9 --- /dev/null +++ b/app/views/notification_mailer/expiration_in_30d.html.erb @@ -0,0 +1,24 @@ +

<%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %>

+ +

+ A tua inscrição como membro da ANSOL expira em 30 dias. +

+ +

+ Em primeiro lugar, queremos agradecer o teu contributo para a ANSOL. + Dependemos exclusivamente da contribuição dos nossos membros para suportar as + nossas actividades, e gostaríamos de continuar a contar com a tua + participação. +

+ +

+ Para estender a tua inscrição por mais um ano, pedimos que faças o pagamento + das quotas até <%= @notification.member.expires_on %>. +

+ +<%= render partial: "payment", locals: { ifthenpay: @link } %> + +

+ Saudações livres,
+ Direcção da ANSOL +

diff --git a/app/views/notification_mailer/expiration_in_30d.text.erb b/app/views/notification_mailer/expiration_in_30d.text.erb new file mode 100644 index 0000000..c02fae4 --- /dev/null +++ b/app/views/notification_mailer/expiration_in_30d.text.erb @@ -0,0 +1,16 @@ +<%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %> + +A tua inscrição como membro da ANSOL expira em 30 dias. + +Em primeiro lugar, queremos agradecer o teu contributo para a ANSOL. +Dependemos exclusivamente da contribuição dos nossos membros para suportar as +nossas actividades, e gostaríamos de continuar a contar com a tua +participação. + +Para estender a tua inscrição por mais um ano, pedimos que faças o pagamento +das quotas até <%= @notification.member.expires_on %>. + +<%= render partial: "payment", locals: { ifthenpay: @link } %> + +Saudações livres, +Direcção da ANSOL diff --git a/app/views/notification_mailer/expiration_in_60d.html.erb b/app/views/notification_mailer/expiration_in_60d.html.erb new file mode 100644 index 0000000..fe15d50 --- /dev/null +++ b/app/views/notification_mailer/expiration_in_60d.html.erb @@ -0,0 +1,24 @@ +

<%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %>

+ +

+ A tua inscrição como membro da ANSOL expira em 60 dias. +

+ +

+ Em primeiro lugar, queremos agradecer o teu contributo para a ANSOL. + Dependemos exclusivamente da contribuição dos nossos membros para suportar as + nossas actividades, e gostaríamos de continuar a contar com a tua + participação. +

+ +

+ Para estender a tua inscrição por mais um ano, pedimos que faças o pagamento + das quotas até <%= @notification.member.expires_on %>. +

+ +<%= render partial: "payment", locals: { ifthenpay: @link } %> + +

+ Saudações livres,
+ Direcção da ANSOL +

diff --git a/app/views/notification_mailer/expiration_in_60d.text.erb b/app/views/notification_mailer/expiration_in_60d.text.erb new file mode 100644 index 0000000..28b31bf --- /dev/null +++ b/app/views/notification_mailer/expiration_in_60d.text.erb @@ -0,0 +1,16 @@ +<%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %> + +A tua inscrição como membro da ANSOL expira em 60 dias. + +Em primeiro lugar, queremos agradecer o teu contributo para a ANSOL. +Dependemos exclusivamente da contribuição dos nossos membros para suportar as +nossas actividades, e gostaríamos de continuar a contar com a tua +participação. + +Para estender a tua inscrição por mais um ano, pedimos que faças o pagamento +das quotas até <%= @notification.member.expires_on %>. + +<%= render partial: "payment", locals: { ifthenpay: @link } %> + +Saudações livres, +Direcção da ANSOL diff --git a/app/views/notification_mailer/expired.html.erb b/app/views/notification_mailer/expired.html.erb new file mode 100644 index 0000000..fc12b72 --- /dev/null +++ b/app/views/notification_mailer/expired.html.erb @@ -0,0 +1,26 @@ +

+ <%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %> +

+ +

+ A tua inscrição como membro da ANSOL vai ser cancelada dentro de 30 dias por + falta de pagamento da contribuição anual. +

+ +

+ Dependemos exclusivamente da contribuição dos nossos membros para suportar as + nossas actividades. Gostaríamos de continuar a contar com a tua participação. +

+ +<%= render partial: "payment", locals: { ifthenpay: @link } %> + +

+ Caso não recebamos o pagamento até dia <%= @notification.member.cancelled_on + %>, cancelaremos permanentemente a tua inscrição. Estamos disponíveis para + esclarecer qualquer dúvida através do endereço direccao@ansol.org. +

+ +

+ Saudações livres,
+ Direcção da ANSOL +

diff --git a/app/views/notification_mailer/expired.text.erb b/app/views/notification_mailer/expired.text.erb new file mode 100644 index 0000000..bb6dc31 --- /dev/null +++ b/app/views/notification_mailer/expired.text.erb @@ -0,0 +1,16 @@ +<%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %> + +A tua inscrição como membro da ANSOL expirou hoje e não recebemos a tua +contribuição anual. + +Dependemos exclusivamente da contribuição dos nossos membros para suportar as +nossas actividades. Gostaríamos de continuar a contar com a tua participação. + +<%= render partial: "payment", locals: { ifthenpay: @link } %> + +Caso não recebamos o pagamento até dia <%= @notification.member.cancelled_on +%>, cancelaremos permanentemente a tua inscrição. Estamos disponíveis para +esclarecer qualquer dúvida através do endereço direccao@ansol.org. + +Saudações livres, +Direcção da ANSOL diff --git a/app/views/notification_mailer/expired_30d_ago.html.erb b/app/views/notification_mailer/expired_30d_ago.html.erb new file mode 100644 index 0000000..8638673 --- /dev/null +++ b/app/views/notification_mailer/expired_30d_ago.html.erb @@ -0,0 +1,26 @@ +

+ <%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %> +

+ +

+ A tua inscrição como membro da ANSOL expirou há um mês e ainda não recebemos + a tua contribuição anual. +

+ +

+ Dependemos exclusivamente da contribuição dos nossos membros para suportar as + nossas actividades. Gostaríamos de continuar a contar com a tua participação. +

+ +<%= render partial: "payment", locals: { ifthenpay: @link } %> + +

+ Caso não recebamos o pagamento até dia <%= @notification.member.cancelled_on + %>, cancelaremos permanentemente a tua inscrição. Estamos disponíveis para + esclarecer qualquer dúvida através do endereço direccao@ansol.org. +

+ +

+ Saudações livres, + Direcção da ANSOL +

diff --git a/app/views/notification_mailer/expired_30d_ago.text.erb b/app/views/notification_mailer/expired_30d_ago.text.erb new file mode 100644 index 0000000..516075a --- /dev/null +++ b/app/views/notification_mailer/expired_30d_ago.text.erb @@ -0,0 +1,16 @@ +<%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %> + +A tua inscrição como membro da ANSOL expirou há um mês e ainda não recebemos +a tua contribuição anual. + +Dependemos exclusivamente da contribuição dos nossos membros para suportar as +nossas actividades. Gostaríamos de continuar a contar com a tua participação. + +<%= render partial: "payment", locals: { ifthenpay: @link } %> + +Caso não recebamos o pagamento até dia <%= @notification.member.cancelled_on +%>, cancelaremos permanentemente a tua inscrição. Estamos disponíveis para +esclarecer qualquer dúvida através do endereço direccao@ansol.org. + +Saudações livres, +Direcção da ANSOL diff --git a/app/views/notification_mailer/expired_60d_ago.html.erb b/app/views/notification_mailer/expired_60d_ago.html.erb new file mode 100644 index 0000000..fc12b72 --- /dev/null +++ b/app/views/notification_mailer/expired_60d_ago.html.erb @@ -0,0 +1,26 @@ +

+ <%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %> +

+ +

+ A tua inscrição como membro da ANSOL vai ser cancelada dentro de 30 dias por + falta de pagamento da contribuição anual. +

+ +

+ Dependemos exclusivamente da contribuição dos nossos membros para suportar as + nossas actividades. Gostaríamos de continuar a contar com a tua participação. +

+ +<%= render partial: "payment", locals: { ifthenpay: @link } %> + +

+ Caso não recebamos o pagamento até dia <%= @notification.member.cancelled_on + %>, cancelaremos permanentemente a tua inscrição. Estamos disponíveis para + esclarecer qualquer dúvida através do endereço direccao@ansol.org. +

+ +

+ Saudações livres,
+ Direcção da ANSOL +

diff --git a/app/views/notification_mailer/expired_60d_ago.text.erb b/app/views/notification_mailer/expired_60d_ago.text.erb new file mode 100644 index 0000000..293d58f --- /dev/null +++ b/app/views/notification_mailer/expired_60d_ago.text.erb @@ -0,0 +1,16 @@ +<%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %> + +A tua inscrição como membro da ANSOL expirou há 60 dias e ainda não recebemos +a tua contribuição anual. + +Dependemos exclusivamente da contribuição dos nossos membros para suportar as +nossas actividades. Gostaríamos de continuar a contar com a tua participação. + +<%= render partial: "payment", locals: { ifthenpay: @link } %> + +Caso não recebamos o pagamento até dia <%= @notification.member.cancelled_on +%>, cancelaremos permanentemente a tua inscrição. Estamos disponíveis para +esclarecer qualquer dúvida através do endereço direccao@ansol.org. + +Saudações livres, +Direcção da ANSOL diff --git a/config/application.rb b/config/application.rb index ff1512b..ce8a4c5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -33,5 +33,6 @@ module Saucy # Don't generate system test files. config.generators.system_tests = nil + config.i18n.default_locale = :pt end end diff --git a/config/environments/development.rb b/config/environments/development.rb index 8d1635e..1317d9a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -41,6 +41,18 @@ Rails.application.configure do config.action_mailer.perform_caching = false + config.action_mailer.delivery_method :smtp + + config.action_mailer.smtp_settings = { + address: ENV['SMTP_ADDRESS'], + port: 587, + domain: ENV['SMTP_DOMAIN'], + user_name: ENV['SMTP_USERNAME'], + password: ENV['SMTP_PASSWORD'], + authentication: 'plain', + enable_starttls_auto: true, + } + # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log diff --git a/config/initializers/rules.rb b/config/initializers/rules.rb new file mode 100644 index 0000000..e69de29 diff --git a/config/locales/en.yml b/config/locales/en.yml index 8ca56fc..3adf8af 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,33 +1,60 @@ -# Files in the config/locales directory are used for internationalization -# and are automatically loaded by Rails. If you want to use locales other -# than English, add the necessary files in this directory. -# -# To use the locales, use `I18n.t`: -# -# I18n.t "hello" -# -# In views, this is aliased to just `t`: -# -# <%= t("hello") %> -# -# To use a different locale, set it with `I18n.locale`: -# -# I18n.locale = :es -# -# This would use the information in config/locales/es.yml. -# -# The following keys must be escaped otherwise they will not be retrieved by -# the default I18n backend: -# -# true, false, on, off, yes, no -# -# Instead, surround them with single quotes. -# -# en: -# "true": "foo" -# -# To learn more, please read the Rails Internationalization guide -# available at https://guides.rubyonrails.org/i18n.html. - en: - hello: "Hello world" + navigation: + members: "Member list" + members: + index: + title: "Members" + actions: + new: "New member" + clear_search: "Clear search" + show: "Show" + edit: "Edit" + new_contribution: "Register contribution" + show: + title: "Member details" + actions: + edit: "Edit" + edit: + title: "Edit member details" + actions: + back_to_show: "Show this member" + edit_dates_warning: "Warning: changing the join/expiration date may trigger the delivery of email notifications regarding pending payments." + new: + title: "Register new member" + attributes: + number: "#" + status: "Status" + email: "Email address" + display_name: "Display name" + joined_on: "Joined on" + expires_on: "Expires on" + category: "Category" + identification_number: "ID number" + address: "Postal address" + status: + any: "Any" + active: "Active" + passive: "Passive" + pending: "Pending" + expired: "Expired" + cancelled: "Cancelled" + category: + any: "Any" + employed: "Employed" + unemployed: "Unemployed" + student: "Student" + retired: "Retired" + notification_mailer: + expiration_in_60d: + subject: "ANSOL - Pagamento anual de quotas" + title: "Pagamento anual de quotas" + expiration_in_30d: + subject: "ANSOL - Prazo para pagamento de quotas vence em 30 dias" + expired: + subject: "ANSOL - Pagamento de quotas pendente" + expired_30d_ago: + subject: "ANSOL - Pagamento de quotas em atraso" + expired_60d_ago: + subject: "ANSOL - Suspensão de inscrição iminente" + cancelled: + subject: "ANSOL - Inscrição cancelada" diff --git a/config/locales/pt.yml b/config/locales/pt.yml new file mode 100644 index 0000000..aa915a4 --- /dev/null +++ b/config/locales/pt.yml @@ -0,0 +1,62 @@ +pt: + navigation: + members: "Lista de membros" + members: + index: + title: "Membros" + actions: + new: "Registar novo membro" + clear_search: "" + show: "Mostrar" + edit: "Editar" + new_contribution: "Registar contribuição" + title: "Acções" + show: + title: "Detalhes de membro" + actions: + edit: "Editar detalhes" + edit_contribution: "Editar" + edit: + title: "Editar detalhes de membro" + actions: + back_to_show: "Show this member" + edit_dates_warning: "Atenção: a alteração das datas de inscrição/expiração podem causar o envio de emails com notificações de atraso de pagamento." + new: + title: "Registar novo membro" + attributes: + number: "#" + status: "Estado" + email: "Endereço de correio electrónico" + display_name: "Nome" + joined_on: "Data de inscrição" + expires_on: "Data de expiração" + category: "Categoria" + identification_number: "N.º de identificação" + address: "Endereço postal" + status: + any: "Qualquer" + active: "Activo" + passive: "Passivo" + pending: "Pendente" + expired: "Expirado" + cancelled: "Cancelado" + category: + any: "Qualquer" + employed: "Empregado" + unemployed: "Desempregado" + student: "Estudante" + retired: "Reformado" + notification_mailer: + expiration_in_60d: + subject: "ANSOL - Pagamento anual de quotas" + expiration_in_30d: + subject: "ANSOL - Inscrição expira em 30 dias" + expired: + subject: "ANSOL - Pagamento de quotas pendente" + expired_30d_ago: + subject: "ANSOL - Pagamento de quotas em atraso" + expired_60d_ago: + subject: "ANSOL - Suspensão de inscrição iminente" + cancelled: + subject: "ANSOL - Inscrição cancelada" + greetings: "Caro(a) %{display_name}" diff --git a/config/routes.rb b/config/routes.rb index 262ffd5..29828f3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,4 +3,10 @@ Rails.application.routes.draw do # Defines the root path route ("/") # root "articles#index" + + resources :members do + resources :contributions, only: [:new, :create] + end + + resources :contributions, only: [:edit, :update] end diff --git a/db/migrate/20220620195513_create_member.rb b/db/migrate/20220620195513_create_member.rb new file mode 100644 index 0000000..97e9456 --- /dev/null +++ b/db/migrate/20220620195513_create_member.rb @@ -0,0 +1,15 @@ +class CreateMember < ActiveRecord::Migration[7.0] + def change + create_table :members, id: :uuid do |t| + t.serial :number, null: false, index: { unique: true } + t.string :email, null: false, index: { unique: true } + t.string :display_name, null: false + t.string :identification_number + t.string :status + t.string :category + t.text :address + + t.timestamps + end + end +end diff --git a/db/migrate/20220620233944_add_dates_to_members.rb b/db/migrate/20220620233944_add_dates_to_members.rb new file mode 100644 index 0000000..05b67f6 --- /dev/null +++ b/db/migrate/20220620233944_add_dates_to_members.rb @@ -0,0 +1,8 @@ +class AddDatesToMembers < ActiveRecord::Migration[7.0] + def change + change_table :members do |t| + t.date :joined_on + t.date :expires_on + end + end +end diff --git a/db/migrate/20220621101236_create_contributions.rb b/db/migrate/20220621101236_create_contributions.rb new file mode 100644 index 0000000..e9fd710 --- /dev/null +++ b/db/migrate/20220621101236_create_contributions.rb @@ -0,0 +1,14 @@ +class CreateContributions < ActiveRecord::Migration[7.0] + def change + create_table :contributions, id: :uuid do |t| + t.references :member, type: :uuid, foreign_key: true, null: false + + t.integer :eurocents, null: false + t.date :payment_on, null: false + t.string :payment_method + t.string :payment_reference + + t.timestamps + end + end +end diff --git a/db/migrate/20220623135702_create_notifications.rb b/db/migrate/20220623135702_create_notifications.rb new file mode 100644 index 0000000..885c309 --- /dev/null +++ b/db/migrate/20220623135702_create_notifications.rb @@ -0,0 +1,14 @@ +class CreateNotifications < ActiveRecord::Migration[7.0] + def change + create_table :notifications, id: :uuid do |t| + t.references :member, type: :uuid, foreign_key: true + t.date :to_be_sent_on, null: false + t.string :template, null: false + + t.string :status, null: false + t.timestamp :sent_at + + t.timestamps + end + end +end diff --git a/db/migrate/20220624134509_add_ifthenpay_links_to_member.rb b/db/migrate/20220624134509_add_ifthenpay_links_to_member.rb new file mode 100644 index 0000000..a2a5b63 --- /dev/null +++ b/db/migrate/20220624134509_add_ifthenpay_links_to_member.rb @@ -0,0 +1,8 @@ +class AddIfthenpayLinksToMember < ActiveRecord::Migration[7.0] + def change + change_table :members do |t| + t.string :regular_ifthenpay_link + t.string :reduced_ifthenpay_link + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 277f025..fead68e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,51 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_06_20_195143) do +ActiveRecord::Schema[7.0].define(version: 2022_06_24_134509) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" + create_table "contributions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "member_id", null: false + t.integer "eurocents", null: false + t.date "payment_on", null: false + t.string "payment_method" + t.string "payment_reference" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["member_id"], name: "index_contributions_on_member_id" + end + + create_table "members", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.serial "number", null: false + t.string "email", null: false + t.string "display_name", null: false + t.string "identification_number" + t.string "status" + t.string "category" + t.text "address" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.date "joined_on" + t.date "expires_on" + t.string "regular_ifthenpay_link" + t.string "reduced_ifthenpay_link" + t.index ["email"], name: "index_members_on_email", unique: true + t.index ["number"], name: "index_members_on_number", unique: true + end + + create_table "notifications", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "member_id" + t.date "to_be_sent_on", null: false + t.string "template", null: false + t.string "status", null: false + t.datetime "sent_at", precision: nil + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["member_id"], name: "index_notifications_on_member_id" + end + + add_foreign_key "contributions", "members" + add_foreign_key "notifications", "members" end diff --git a/lib/tasks/saucy.rake b/lib/tasks/saucy.rake new file mode 100644 index 0000000..4d4e184 --- /dev/null +++ b/lib/tasks/saucy.rake @@ -0,0 +1,14 @@ +desc "Application specific tasks" +namespace :saucy do + desc "Background sync operations" + task sync: :environment do + Member.generate_all_missing_ifthenpay_links! + Member.reset_all_status! + Member.regenerate_all_notifications + end + + desc "Send daily email notifications" + task notify: :environment do + Notification.send_scheduled_for_today + end +end diff --git a/test/controllers/members_controller_test.rb b/test/controllers/members_controller_test.rb new file mode 100644 index 0000000..da7b390 --- /dev/null +++ b/test/controllers/members_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class MembersControllerTest < ActionDispatch::IntegrationTest + #setup do + # @member = members(:one) + #end + + #test "should get index" do + # get members_url + # assert_response :success + #end + + #test "should get new" do + # get new_member_url + # assert_response :success + #end + + #test "should create member" do + # assert_difference("Member.count") do + # post members_url, params: { member: { } } + # end + + # assert_redirected_to member_url(Member.last) + #end + + #test "should show member" do + # get member_url(@member) + # assert_response :success + #end + + #test "should get edit" do + # get edit_member_url(@member) + # assert_response :success + #end + + #test "should update member" do + # patch member_url(@member), params: { member: { } } + # assert_redirected_to member_url(@member) + #end + + #test "should destroy member" do + # assert_difference("Member.count", -1) do + # delete member_url(@member) + # end + + # assert_redirected_to members_url + #end +end diff --git a/test/fixtures/notifications.yml b/test/fixtures/notifications.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/notifications.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/mailers/notification_mailer_test.rb b/test/mailers/notification_mailer_test.rb new file mode 100644 index 0000000..6f9fed6 --- /dev/null +++ b/test/mailers/notification_mailer_test.rb @@ -0,0 +1,52 @@ +require "test_helper" + +class NotificationMailerTest < ActionMailer::TestCase + test "expiration_in_60d" do + mail = NotificationMailer.expiration_in_60d + assert_equal "Expiration in 60d", mail.subject + assert_equal ["to@example.org"], mail.to + assert_equal ["from@example.com"], mail.from + assert_match "Hi", mail.body.encoded + end + + test "expiration_in_30d" do + mail = NotificationMailer.expiration_in_30d + assert_equal "Expiration in 30d", mail.subject + assert_equal ["to@example.org"], mail.to + assert_equal ["from@example.com"], mail.from + assert_match "Hi", mail.body.encoded + end + + test "expired" do + mail = NotificationMailer.expired + assert_equal "Expired", mail.subject + assert_equal ["to@example.org"], mail.to + assert_equal ["from@example.com"], mail.from + assert_match "Hi", mail.body.encoded + end + + test "expired_30d_ago" do + mail = NotificationMailer.expired_30d_ago + assert_equal "Expired 30d ago", mail.subject + assert_equal ["to@example.org"], mail.to + assert_equal ["from@example.com"], mail.from + assert_match "Hi", mail.body.encoded + end + + test "expired_60d_ago" do + mail = NotificationMailer.expired_60d_ago + assert_equal "Expired 60d ago", mail.subject + assert_equal ["to@example.org"], mail.to + assert_equal ["from@example.com"], mail.from + assert_match "Hi", mail.body.encoded + end + + test "cancelled" do + mail = NotificationMailer.cancelled + assert_equal "Cancelled", mail.subject + assert_equal ["to@example.org"], mail.to + assert_equal ["from@example.com"], mail.from + assert_match "Hi", mail.body.encoded + end + +end diff --git a/test/mailers/previews/notification_mailer_preview.rb b/test/mailers/previews/notification_mailer_preview.rb new file mode 100644 index 0000000..5e67035 --- /dev/null +++ b/test/mailers/previews/notification_mailer_preview.rb @@ -0,0 +1,34 @@ +# Preview all emails at http://localhost:3000/rails/mailers/notification_mailer +class NotificationMailerPreview < ActionMailer::Preview + + # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/expiration_in_60d + def expiration_in_60d + NotificationMailer.expiration_in_60d + end + + # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/expiration_in_30d + def expiration_in_30d + NotificationMailer.expiration_in_30d + end + + # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/expired + def expired + NotificationMailer.expired + end + + # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/expired_30d_ago + def expired_30d_ago + NotificationMailer.expired_30d_ago + end + + # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/expired_60d_ago + def expired_60d_ago + NotificationMailer.expired_60d_ago + end + + # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/cancelled + def cancelled + NotificationMailer.cancelled + end + +end diff --git a/test/models/member_test.rb b/test/models/member_test.rb new file mode 100644 index 0000000..57effa2 --- /dev/null +++ b/test/models/member_test.rb @@ -0,0 +1,26 @@ +require "test_helper" + +class MemberTest < ActiveSupport::TestCase + setup do + @member = Member.create!( + email: 'dsfargeg@example.com', + display_name: 'dsfargeg', + joined_on: Date.today, + expires_on: Date.today + 1.year + ) + end + + test "no expired in the first year and 90 days" do + (1.year + 90.days).in_days.to_i.times do |n| + Timecop.freeze(Date.today + n.days) do + assert_not_equal @member.expected_status, :cancelled + end + end + end + + test "expired after 1 year and 90 days" do + Timecop.freeze(Date.today + 1.year + 90.days) do + assert_equal @member.expected_status, :cancelled + end + end +end diff --git a/test/models/notification_test.rb b/test/models/notification_test.rb new file mode 100644 index 0000000..a76e08d --- /dev/null +++ b/test/models/notification_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class NotificationTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end