Add first iteration

main
Hugo Peixoto 5 months ago
parent f97b01dd43
commit 57e976ef96
  1. 2
      Gemfile
  2. 7
      Gemfile.lock
  3. 6
      app/assets/stylesheets/application.css
  4. 54
      app/controllers/contributions_controller.rb
  5. 72
      app/controllers/members_controller.rb
  6. 3
      app/helpers/application_helper.rb
  7. 24
      app/helpers/members_helper.rb
  8. 19
      app/lib/if_then_pay.rb
  9. 2
      app/mailers/application_mailer.rb
  10. 68
      app/mailers/notification_mailer.rb
  11. 3
      app/models/contribution.rb
  12. 88
      app/models/member.rb
  13. 20
      app/models/notification.rb
  14. 22
      app/views/contributions/edit.html.erb
  15. 48
      app/views/contributions/new.html.erb
  16. 4
      app/views/layouts/application.html.erb
  17. 8
      app/views/layouts/mailer.html.erb
  18. 0
      app/views/members/_form.html.erb
  19. 2
      app/views/members/_member.html.erb
  20. 59
      app/views/members/edit.html.erb
  21. 44
      app/views/members/index.html.erb
  22. 27
      app/views/members/new.html.erb
  23. 40
      app/views/members/show.html.erb
  24. 22
      app/views/notification_mailer/_payment.html.erb
  25. 14
      app/views/notification_mailer/_payment.text.erb
  26. 30
      app/views/notification_mailer/cancelled.html.erb
  27. 17
      app/views/notification_mailer/cancelled.text.erb
  28. 24
      app/views/notification_mailer/expiration_in_30d.html.erb
  29. 16
      app/views/notification_mailer/expiration_in_30d.text.erb
  30. 24
      app/views/notification_mailer/expiration_in_60d.html.erb
  31. 16
      app/views/notification_mailer/expiration_in_60d.text.erb
  32. 26
      app/views/notification_mailer/expired.html.erb
  33. 16
      app/views/notification_mailer/expired.text.erb
  34. 26
      app/views/notification_mailer/expired_30d_ago.html.erb
  35. 16
      app/views/notification_mailer/expired_30d_ago.text.erb
  36. 26
      app/views/notification_mailer/expired_60d_ago.html.erb
  37. 16
      app/views/notification_mailer/expired_60d_ago.text.erb
  38. 1
      config/application.rb
  39. 12
      config/environments/development.rb
  40. 0
      config/initializers/rules.rb
  41. 91
      config/locales/en.yml
  42. 62
      config/locales/pt.yml
  43. 6
      config/routes.rb
  44. 15
      db/migrate/20220620195513_create_member.rb
  45. 8
      db/migrate/20220620233944_add_dates_to_members.rb
  46. 14
      db/migrate/20220621101236_create_contributions.rb
  47. 14
      db/migrate/20220623135702_create_notifications.rb
  48. 8
      db/migrate/20220624134509_add_ifthenpay_links_to_member.rb
  49. 44
      db/schema.rb
  50. 14
      lib/tasks/saucy.rake
  51. 48
      test/controllers/members_controller_test.rb
  52. 11
      test/fixtures/notifications.yml
  53. 52
      test/mailers/notification_mailer_test.rb
  54. 34
      test/mailers/previews/notification_mailer_preview.rb
  55. 26
      test/models/member_test.rb
  56. 7
      test/models/notification_test.rb

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

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

@ -1 +1,7 @@
/* Application styles */
table { border: 1px solid black; }
td, th { padding: 10px; }
.new_contribution_form { max-width: 600px; }

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

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

@ -1,2 +1,5 @@
module ApplicationHelper
def member_status(status)
t("members.status.#{status}")
end
end

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

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

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

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

@ -0,0 +1,3 @@
class Contribution < ApplicationRecord
belongs_to :member
end

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

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

@ -0,0 +1,22 @@
editando contriboot
<%= form_with(model: @contribution) do |form| %>
<table>
<tr>
<td>Amount</td>
<td>€<%= @contribution.eurocents %></td>
</tr>
<tr>
<td>Payment date</td>
<td>€<%= @contribution.payment_on %></td>
</tr>
<tr>
<td>Payment method</td>
<td><%= @contribution.payment_method %></td>
</tr>
<tr>
<td>Payment reference</td>
<td><%= @contribution.payment_reference %></td>
</tr>
</table>
<% end %>

@ -0,0 +1,48 @@
<h1>Registering contribution for <%= @member.display_name %></h1>
<table>
<tr><td>Member number</td><td><%= @member.number %></td></tr>
<tr><td>Joined on</td><td><%= @member.joined_on %></td></tr>
<tr><td>Expires on</td><td><%= @member.expires_on %></td></tr>
</table>
<%= form_with(model: [@member, @contribution]) do |form| %>
<table class="new_contribution_form">
<tr>
<td><label for="contribution_eurocents">Amount</label></td>
<td><%= form.number_field :eurocents %></td>
</tr>
<tr>
<td><label for="contribution_payment_on">Payment date</label></td>
<td><%= form.date_field :payment_on %></td>
</tr>
<tr>
<td><label for="contribution_payment_method">Payment method</label></td>
<td><%= form.select :payment_method, %w[iban mbway multibanco] %></td>
</tr>
<tr>
<td><label for="contribution_payment_reference">Referência</label></td>
<td><%= form.text_field :payment_reference %></td>
</tr>
<tr>
<td colspan=2>
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:
</td>
</tr>
<tr>
<td><label for="member_expires_on">Nova data de expiração</label></td>
<td>
<%= form.date_field :overriden_expires_on %>
</td>
</tr>
</table>
<div>
<%= form.submit %>
</div>
<% end %>

@ -11,6 +11,10 @@
</head>
<body>
<nav>
<%= link_to t('navigation.members'), members_path %>
</nav>
<%= yield %>
</body>
</html>

@ -8,6 +8,12 @@
</head>
<body>
<%= yield %>
<div style="max-width: 600px;">
<div style="background-color: #041952; padding: 20px">
<img src="https://hugopeixoto.net/images/ansol-logo-white.png?xxx" style="margin: 0px auto; max-width: 400px; display: block; color: white" alt="ANSOL" />
</div>
<%= yield %>
</div>
</body>
</html>

@ -0,0 +1,2 @@
<div id="<%= dom_id member %>">
</div>

@ -0,0 +1,59 @@
<h1><%= t('members.edit.title') %></h1>
<%= form_with(model: @member) do |form| %>
<% if @member.errors.any? %>
<div style="color: red">
<h2><%= pluralize(@member.errors.count, "error") %> prohibited this member from being saved:</h2>
<ul>
<% @member.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<table>
<tr>
<td><label><%= t('members.attributes.display_name') %></label></td>
<td><%= form.text_field :display_name, required: true %></label></td>
</tr>
<tr>
<td><label><%= t('members.attributes.email') %></label></td>
<td><%= form.email_field :email, required: true %></label></td>
</tr>
<tr>
<td><label><%= t('members.attributes.category') %></label></td>
<td><%= form.select :category, %w{student retired unemployed employed} %></label></td>
</tr>
<tr>
<td><label><%= t('members.attributes.identification_number') %></label></td>
<td><%= form.text_field :identification_number %></label></td>
</tr>
<tr>
<td><label><%= t('members.attributes.address') %></label></td>
<td><%= form.text_area :address %></label></td>
</tr>
<tr>
<td colspan="2"><%= t('members.edit.edit_dates_warning') %></td>
</tr>
<tr>
<td><label for="member_joined_on"><%= t('members.attributes.joined_on') %></label></td>
<td><%= form.date_field :joined_on %></td>
</tr>
<tr>
<td><label for="member_expires_on"><%= t('members.attributes.expires_on') %></label></td>
<td><%= form.date_field :expires_on %></td>
</tr>
</table>
<div>
<%= form.submit %>
</div>
<% end %>
<br>
<div>
<%= link_to t('members.edit.actions.back_to_show'), @member %>
</div>

@ -0,0 +1,44 @@
<p style="color: green"><%= notice %></p>
<h1><%= t 'members.index.title' %></h1>
<%= 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 %>
<table id="members">
<thead>
<tr>
<th><%= link_to_current_with_sort t('members.attributes.number'), 'number.asc' %></th>
<th><%= link_to_current_with_sort t('members.attributes.status'), 'status.asc' %></th>
<th><%= link_to_current_with_sort t('members.attributes.email'), 'email.asc' %></th>
<th><%= link_to_current_with_sort t('members.attributes.display_name'), 'display_name.asc' %></th>
<th><%= link_to_current_with_sort t('members.attributes.joined_on'), 'joined_on.asc' %></th>
<th><%= link_to_current_with_sort t('members.attributes.expires_on'), 'expires_on.asc' %></th>
<th><%= t('members.index.actions.title') %></th>
</tr>
</thead>
<% @members.each do |member| %>
<tr id="<%= dom_id member %>">
<td><%= member.number %></td>
<td><%= member_status(member.status) %></td>
<td><%= member.email %></td>
<td><%= member.display_name %></td>
<td><%= member.joined_on %></td>
<td><%= member.expires_on %></td>
<td>
<%= 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) %>
</td>
</tr>
<% end %>
</table>

@ -0,0 +1,27 @@
<h1><%= t('members.new.title') %></h1>
<%= form_with(model: @member) do |form| %>
<% if @member.errors.any? %>
<div style="color: red">
<h2><%= pluralize(@member.errors.count, "error") %> prohibited this member from being saved:</h2>
<ul>
<% @member.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<table>
<tr><td><label><%= t('members.attributes.display_name') %></label></td><td><%= form.text_field :display_name, required: true %></label></td></tr>
<tr><td><label><%= t('members.attributes.email') %></label></td><td><%= form.email_field :email, required: true %></label></td></tr>
<tr><td><label><%= t('members.attributes.category') %></label></td><td><%= form.select :category, %w{student retired unemployed employed} %></label></td></tr>
<tr><td><label><%= t('members.attributes.identification_number') %></label></td><td><%= form.text_field :identification_number %></label></td></tr>
<tr><td><label><%= t('members.attributes.address') %></label></td><td><%= form.text_area :address %></label></td></tr>
</table>
<div>
<%= form.submit %>
</div>
<% end %>

@ -0,0 +1,40 @@
<p style="color: green"><%= notice %></p>
<h1><%= t('members.show.title') %></h1>
<table>
<tr><td><%= t('members.attributes.display_name') %></td><td><%= @member.display_name %></td></tr>
<tr><td><%= t('members.attributes.email') %></td><td><%= @member.email %></td></tr>
<tr><td><%= t('members.attributes.category') %></td><td><%= @member.category %></td></tr>
<tr><td><%= t('members.attributes.identification_number') %></td><td><%= @member.identification_number %></td></tr>
<tr><td><%= t('members.attributes.address') %></td><td><%= simple_format @member.address %></td></tr>
<tr><td><%= t('members.attributes.joined_on') %></td><td><%= @member.joined_on %></td></tr>
<tr><td><%= t('members.attributes.expires_on') %></td><td><%= @member.expires_on %></td></tr>
<tr><td><%= t('members.attributes.status') %></td><td><%= @member.status %></td></tr>
</table>
<div>
<%= link_to t('members.show.actions.edit'), edit_member_path(@member) %>
</div>
<h2><%= t('members.show.contribution_history') %></h2>
<table>
<tr>
<th>Payment date</th>
<th>Payment method</th>
<th>Payment reference</th>
<th>Amount</th>
</tr>
<% @member.contributions.each do |contribution| %>
<tr>
<td><%= contribution.payment_on %></td>
<td><%= contribution.payment_method %></td>
<td><%= contribution.payment_reference %></td>
<td>€<%= contribution.eurocents %></td>
<td>
<%= link_to t('members.show.actions.edit_contribution'), edit_contribution_path(contribution) %>
</td>
</tr>
<% end %>
</table>

@ -0,0 +1,22 @@
<p>
Aceitamos pagamento via transferência bancária, referência multibanco ou
MBWAY:
</p>
<ul>
<li>Valor: 30.00€</li>
<li>Transferência bancária: <strong>PT50 0035 2178 00027478430 14</strong></li>
<li>Multibanco ou MBWAY: <a href="<%= ifthenpay %>"><%= ifthenpay %></a></li>
</ul>
<p>
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.
</p>
<p>
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.
</p>

@ -0,0 +1,14 @@
Aceitamos pagamento via transferência bancária, referência multibanco ou
MBWAY:
* Valor: 30.00€
* Transferência bancária: <strong>PT50 0035 2178 00027478430 14</strong>
* Multibanco ou MBWAY: <a href="<%= @link %>"><%= @link %></a>
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.

@ -0,0 +1,30 @@
<p>
<%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %>
</p>
<p>
Como não recebemos o pagamento anual das quotas da ANSOL, a tua inscrição foi
cancelada.
</p>
<p>
Vamos revogar em breve os teus acessos à infraestrutura da associação
exclusiva para membros (nextcloud, mailing list, sala de Matrix, etc).
</p>
<p>
Esperamos poder voltar a merecer o teu apoio no futuro. Podes reinscrever-te
a qualquer altura através do formulário disponível em
<a href="https://ansol.org/inscricao">https://ansol.org/inscricao</a>.
</p>
<p>
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.
</p>
<p>
Saudações livres,<br>
Direcção da ANSOL
</p>

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

@ -0,0 +1,24 @@
<p><%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %></p>
<p>
A tua inscrição como membro da ANSOL expira em 30 dias.
</p>
<p>
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.
</p>
<p>
Para estender a tua inscrição por mais um ano, pedimos que faças o pagamento
das quotas até <strong><%= @notification.member.expires_on %></strong>.
</p>
<%= render partial: "payment", locals: { ifthenpay: @link } %>
<p>
Saudações livres,<br>
Direcção da ANSOL
</p>

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

@ -0,0 +1,24 @@
<p><%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %></p>
<p>
A tua inscrição como membro da ANSOL expira em 60 dias.
</p>
<p>
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.
</p>
<p>
Para estender a tua inscrição por mais um ano, pedimos que faças o pagamento
das quotas até <strong><%= @notification.member.expires_on %></strong>.
</p>
<%= render partial: "payment", locals: { ifthenpay: @link } %>
<p>
Saudações livres,<br>
Direcção da ANSOL
</p>

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

@ -0,0 +1,26 @@
<p>
<%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %>
</p>
<p>
A tua inscrição como membro da ANSOL vai ser cancelada dentro de 30 dias por
falta de pagamento da contribuição anual.
</p>
<p>
Dependemos exclusivamente da contribuição dos nossos membros para suportar as
nossas actividades. Gostaríamos de continuar a contar com a tua participação.
</p>
<%= render partial: "payment", locals: { ifthenpay: @link } %>
<p>
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.
</p>
<p>
Saudações livres,<br>
Direcção da ANSOL
</p>

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

@ -0,0 +1,26 @@
<p>
<%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %>
</p>
<p>
A tua inscrição como membro da ANSOL expirou há um mês e ainda não recebemos
a tua contribuição anual.
</p>
<p>
Dependemos exclusivamente da contribuição dos nossos membros para suportar as
nossas actividades. Gostaríamos de continuar a contar com a tua participação.
</p>
<%= render partial: "payment", locals: { ifthenpay: @link } %>
<p>
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.
</p>
<p>
Saudações livres,
Direcção da ANSOL
</p>

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

@ -0,0 +1,26 @@
<p>
<%= t('notification_mailer.greetings', display_name: @notification.member.display_name) %>
</p>
<p>
A tua inscrição como membro da ANSOL vai ser cancelada dentro de 30 dias por
falta de pagamento da contribuição anual.
</p>
<p>
Dependemos exclusivamente da contribuição dos nossos membros para suportar as
nossas actividades. Gostaríamos de continuar a contar com a tua participação.
</p>
<%= render partial: "payment", locals: { ifthenpay: @link } %>
<p>
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.
</p>
<p>
Saudações livres,<br>
Direcção da ANSOL
</p>

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

@ -33,5 +33,6 @@ module Saucy
# Don't generate system test files.
config.generators.system_tests = nil
config.i18n.default_locale = :pt
end
end

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

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

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

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

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

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

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

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

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

44
db/schema.rb generated

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

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

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

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