Add first iteration

This commit is contained in:
Hugo Peixoto 2022-06-25 13:48:46 +01:00
parent f97b01dd43
commit 57e976ef96
56 changed files with 1343 additions and 35 deletions

View File

@ -11,9 +11,11 @@ gem "puma", "~> 5.0"
gem "rails", "~> 7.0.3" gem "rails", "~> 7.0.3"
gem "dotenv-rails" gem "dotenv-rails"
gem "pundit" gem "pundit"
gem "ransack"
group :development, :test do group :development, :test do
gem "debug", platforms: %i[ mri mingw x64_mingw ] gem "debug", platforms: %i[ mri mingw x64_mingw ]
gem "timecop"
end end
group :development do group :development do

View File

@ -159,10 +159,15 @@ GEM
thor (~> 1.0) thor (~> 1.0)
zeitwerk (~> 2.5) zeitwerk (~> 2.5)
rake (13.0.6) rake (13.0.6)
ransack (3.2.1)
activerecord (>= 6.1.5)
activesupport (>= 6.1.5)
i18n
reline (0.3.1) reline (0.3.1)
io-console (~> 0.5) io-console (~> 0.5)
strscan (3.0.3) strscan (3.0.3)
thor (1.2.1) thor (1.2.1)
timecop (0.9.5)
timeout (0.3.0) timeout (0.3.0)
tzinfo (2.0.4) tzinfo (2.0.4)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
@ -189,6 +194,8 @@ DEPENDENCIES
puma (~> 5.0) puma (~> 5.0)
pundit pundit
rails (~> 7.0.3) rails (~> 7.0.3)
ransack
timecop
web-console web-console
RUBY VERSION RUBY VERSION

View File

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

View File

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

View File

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

View File

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

View File

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

19
app/lib/if_then_pay.rb Normal file
View File

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

View File

@ -1,4 +1,4 @@
class ApplicationMailer < ActionMailer::Base 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" layout "mailer"
end end

View File

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

View File

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

88
app/models/member.rb Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,12 @@
</head> </head>
<body> <body>
<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 %> <%= yield %>
</div>
</body> </body>
</html> </html>

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,6 +41,18 @@ Rails.application.configure do
config.action_mailer.perform_caching = false 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. # Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log config.active_support.deprecation = :log

View File

View File

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

62
config/locales/pt.yml Normal file
View File

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

View File

@ -3,4 +3,10 @@ Rails.application.routes.draw do
# Defines the root path route ("/") # Defines the root path route ("/")
# root "articles#index" # root "articles#index"
resources :members do
resources :contributions, only: [:new, :create]
end
resources :contributions, only: [:edit, :update]
end end

View File

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

View File

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

View File

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

View File

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

View File

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

@ -10,9 +10,51 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto" enable_extension "pgcrypto"
enable_extension "plpgsql" 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 end

14
lib/tasks/saucy.rake Normal file
View File

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

View File

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

11
test/fixtures/notifications.yml vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
require "test_helper"
class NotificationTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end