diff --git a/backend/app/controllers/campaigns_controller.rb b/backend/app/controllers/campaigns_controller.rb index 513fef35..9e837d4d 100644 --- a/backend/app/controllers/campaigns_controller.rb +++ b/backend/app/controllers/campaigns_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class CampaignsController < ApplicationController - before_action :authenticate_admin! - before_action :set_campaign, only: %i[show update destroy] + before_action :authenticate_admin!, except: %i[index show] + before_action :set_campaign, only: %i[show admin_show update destroy] wrap_parameters format: :json, include: %w[name description promisedAmount start end primaryDonorId @@ -12,27 +12,33 @@ def index @campaigns = Campaign.all end + def admin_index + @campaigns = Campaign.all + end + def show; end + def admin_show; end + def create @campaign = Campaign.create!(campaign_params) add_success_message "Campaign \"#{@campaign.name}\" successfully created!" - render :show, status: :created, location: @campaign + render :response, status: :created, location: @campaign end def update @campaign.update!(campaign_params) add_success_message "Campaign \"#{@campaign.name}\" successfully updated!" - render :show, status: :ok, location: @campaign + render :response, status: :ok, location: @campaign end def destroy @campaign.destroy! add_success_message "Campaign \"#{@campaign.name}\" successfully deleted!" - render :show, status: :ok, location: @campaign + render :response, status: :ok, location: @campaign end private diff --git a/backend/app/models/campaign.rb b/backend/app/models/campaign.rb index b4ebcf9c..411cd415 100644 --- a/backend/app/models/campaign.rb +++ b/backend/app/models/campaign.rb @@ -18,7 +18,7 @@ class Campaign < ApplicationRecord validates :description, presence: true, allow_blank: false validates :start, presence: true validates :end, comparison: { greater_than: :start } - validates :campaign_charities, presence: true + validates :campaign_charities, length: { minimum: 1, maximum: 5 } validates :promised_amount, final: true validates :coupon_denomination, final: true validates :image, @@ -26,6 +26,23 @@ class Campaign < ApplicationRecord message: 'is mot of a supported file type. Please upload a PNG, JPG or JPEG file.' }, size: { less_than: 1.megabytes, message: 'must be less than 1MB.' } + def donation_breakdown + primary_donor_amount = num_redeemed_coupons * coupon_denomination + secondary_donors_amount = secondary_donations.sum(&:amount) + total_amount = (primary_donor_amount + secondary_donors_amount).to_f + primary_donor_fraction = primary_donor_amount / total_amount + secondary_donors_fraction = secondary_donors_amount / total_amount + + { primary_donor_amount: primary_donor_amount, + primary_donor_fraction: primary_donor_fraction, + secondary_donors_amount: secondary_donors_amount, + secondary_donors_fraction: secondary_donors_fraction } + end + + def num_redeemed_coupons + coupons.count(&:redeemed?) + end + private def num_coupons diff --git a/backend/app/models/campaign_charity.rb b/backend/app/models/campaign_charity.rb index d13b34b7..b89cf60c 100644 --- a/backend/app/models/campaign_charity.rb +++ b/backend/app/models/campaign_charity.rb @@ -4,6 +4,20 @@ class CampaignCharity < ApplicationRecord belongs_to :campaign belongs_to :charity has_many :secondary_donations, dependent: :destroy + has_many :coupons, through: :secondary_donations validates :giving_sg_url, presence: true, allow_blank: false, format: { with: URI::DEFAULT_PARSER.make_regexp } + + def donation_breakdown + primary_donor_amount = coupons.count * campaign.coupon_denomination + secondary_donors_amount = secondary_donations.sum(&:amount) + total_amount = (primary_donor_amount + secondary_donors_amount).to_f + primary_donor_fraction = primary_donor_amount / total_amount + secondary_donors_fraction = secondary_donors_amount / total_amount + + { primary_donor_amount: primary_donor_amount, + primary_donor_fraction: primary_donor_fraction, + secondary_donors_amount: secondary_donors_amount, + secondary_donors_fraction: secondary_donors_fraction } + end end diff --git a/backend/app/models/coupon.rb b/backend/app/models/coupon.rb index 82b0fafe..b724f5cd 100644 --- a/backend/app/models/coupon.rb +++ b/backend/app/models/coupon.rb @@ -4,7 +4,7 @@ class Coupon < ApplicationRecord NUM_ALPHANUMERIC_CHARS_IN_TOKEN = 6 belongs_to :campaign - has_one :secondary_donation, required: false, dependent: nil + has_one :secondary_donation, required: false, dependent: :nullify validates :url_token, presence: true, uniqueness: true validates :denomination, presence: true, numericality: { only_integer: true, greater_than: 0 } diff --git a/backend/app/views/campaign_charities/_base.json.jbuilder b/backend/app/views/campaign_charities/_base.json.jbuilder new file mode 100644 index 00000000..e230c56f --- /dev/null +++ b/backend/app/views/campaign_charities/_base.json.jbuilder @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +json.id campaign_charity.id +json.charity do + json.id campaign_charity.charity.id +end +json.givingSgUrl campaign_charity.giving_sg_url diff --git a/backend/app/views/campaign_charities/_campaign_charity.json.jbuilder b/backend/app/views/campaign_charities/_campaign_charity.json.jbuilder new file mode 100644 index 00000000..25c33940 --- /dev/null +++ b/backend/app/views/campaign_charities/_campaign_charity.json.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +json.partial! 'campaign_charities/base', campaign_charity: campaign_charity +json.charity do + json.partial! 'charities/list', charity: campaign_charity.charity +end diff --git a/backend/app/views/campaign_charities/_donation.json.jbuilder b/backend/app/views/campaign_charities/_donation.json.jbuilder new file mode 100644 index 00000000..b4de79b1 --- /dev/null +++ b/backend/app/views/campaign_charities/_donation.json.jbuilder @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +json.partial! 'campaign_charities/campaign_charity', campaign_charity: campaign_charity +json.partial! 'secondary_donations/breakdown', donation_breakdown: campaign_charity.donation_breakdown diff --git a/backend/app/views/campaign_charities/_donation_public.json.jbuilder b/backend/app/views/campaign_charities/_donation_public.json.jbuilder new file mode 100644 index 00000000..46061b4d --- /dev/null +++ b/backend/app/views/campaign_charities/_donation_public.json.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +json.partial! 'campaign_charities/campaign_charity', campaign_charity: campaign_charity +json.partial! 'secondary_donations/breakdown', donation_breakdown: campaign_charity.donation_breakdown + +json.attributes!.delete('givingSgUrl') diff --git a/backend/app/views/campaigns/_base.json.jbuilder b/backend/app/views/campaigns/_base.json.jbuilder new file mode 100644 index 00000000..c8ce7ad1 --- /dev/null +++ b/backend/app/views/campaigns/_base.json.jbuilder @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +json.id campaign.id +json.name campaign.name +json.description campaign.description +json.promisedAmount campaign.promised_amount +json.start campaign.start +json.end campaign.end +json.imageBase64 encoded_file_data_url(campaign.image) + +json.charities campaign.campaign_charities do |campaign_charity| + json.partial! 'campaign_charities/base', campaign_charity: campaign_charity +end + +json.primaryDonor do + json.partial! 'primary_donors/primary_donor', primary_donor: campaign.primary_donor +end + +json.interestId campaign.interest_id diff --git a/backend/app/views/campaigns/admin_index.json.jbuilder b/backend/app/views/campaigns/admin_index.json.jbuilder new file mode 100644 index 00000000..2531ab95 --- /dev/null +++ b/backend/app/views/campaigns/admin_index.json.jbuilder @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +json.array! @campaigns do |campaign| + json.id campaign.id + json.name campaign.name + json.promisedAmount campaign.promised_amount + json.start campaign.start + json.end campaign.end + json.primaryDonor do + json.id campaign.primary_donor.id + json.name campaign.primary_donor.name + end +end diff --git a/backend/app/views/campaigns/admin_show.json.jbuilder b/backend/app/views/campaigns/admin_show.json.jbuilder new file mode 100644 index 00000000..efa46279 --- /dev/null +++ b/backend/app/views/campaigns/admin_show.json.jbuilder @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +json.partial! 'campaigns/base', campaign: @campaign + +json.donations do + json.partial! 'secondary_donations/breakdown', donation_breakdown: @campaign.donation_breakdown +end + +json.charities [] # ignore the charities from the base partial, since Jbuilder merges instead of overwriting +json.charities do + json.array! @campaign.campaign_charities, partial: 'campaign_charities/donation', as: :campaign_charity +end + +json.coupons do + json.array! @campaign.coupons, partial: 'coupons/list', as: :coupon +end diff --git a/backend/app/views/campaigns/index.json.jbuilder b/backend/app/views/campaigns/index.json.jbuilder index c0d95115..265e55ff 100644 --- a/backend/app/views/campaigns/index.json.jbuilder +++ b/backend/app/views/campaigns/index.json.jbuilder @@ -1,3 +1,18 @@ # frozen_string_literal: true -json.array! @campaigns, partial: 'campaigns/campaign', as: :campaign +json.array! @campaigns do |campaign| + json.id campaign.id + json.name campaign.name + json.description campaign.description + json.imageBase64 encoded_file_data_url(campaign.image) + + json.charities do + json.array! campaign.charities, partial: 'charities/list', as: :charity + end + + json.donations do + json.partial! 'secondary_donations/breakdown', donation_breakdown: campaign.donation_breakdown + end + + json.couponsRedeemedCount campaign.num_redeemed_coupons +end diff --git a/backend/app/views/campaigns/response.json.jbuilder b/backend/app/views/campaigns/response.json.jbuilder new file mode 100644 index 00000000..f409ba0f --- /dev/null +++ b/backend/app/views/campaigns/response.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.partial! 'campaigns/base', campaign: @campaign diff --git a/backend/app/views/campaigns/show.json.jbuilder b/backend/app/views/campaigns/show.json.jbuilder index 56092f72..2a35de5f 100644 --- a/backend/app/views/campaigns/show.json.jbuilder +++ b/backend/app/views/campaigns/show.json.jbuilder @@ -1,3 +1,12 @@ # frozen_string_literal: true -json.partial! 'campaigns/campaign', campaign: @campaign +json.partial! 'campaigns/base', campaign: @campaign + +json.donations do + json.partial! 'secondary_donations/breakdown', donation_breakdown: @campaign.donation_breakdown +end + +json.charities [] # ignore the charities from the base partial, since Jbuilder merges instead of overwriting +json.charities do + json.array! @campaign.campaign_charities, partial: 'campaign_charities/donation_public', as: :campaign_charity +end diff --git a/backend/app/views/charities/_list.json.jbuilder b/backend/app/views/charities/_list.json.jbuilder new file mode 100644 index 00000000..9269bd9a --- /dev/null +++ b/backend/app/views/charities/_list.json.jbuilder @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +json.partial! 'charities/minimal', charity: charity +json.logoBase64 encoded_file_data_url(charity.logo) diff --git a/backend/app/views/charities/_minimal.json.jbuilder b/backend/app/views/charities/_minimal.json.jbuilder new file mode 100644 index 00000000..5c9687a8 --- /dev/null +++ b/backend/app/views/charities/_minimal.json.jbuilder @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +json.id charity.id +json.name charity.name diff --git a/backend/app/views/coupons/_base.json.jbuilder b/backend/app/views/coupons/_base.json.jbuilder new file mode 100644 index 00000000..8b4ff998 --- /dev/null +++ b/backend/app/views/coupons/_base.json.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +json.id coupon.id +json.urlToken coupon.url_token +json.denomination coupon.denomination +json.campaignId coupon.campaign_id diff --git a/backend/app/views/coupons/_list.json.jbuilder b/backend/app/views/coupons/_list.json.jbuilder new file mode 100644 index 00000000..50afea5f --- /dev/null +++ b/backend/app/views/coupons/_list.json.jbuilder @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +json.partial! 'coupons/base', coupon: coupon + +if coupon.secondary_donation.present? + json.secondaryDonation do + json.partial! 'secondary_donations/secondary_donation', secondary_donation: coupon.secondary_donation + end + + json.charity do + json.partial! 'charities/minimal', charity: coupon.secondary_donation.campaign_charity.charity + end +else + json.secondaryDonation nil + json.charity nil +end diff --git a/backend/app/views/secondary_donations/_breakdown.json.jbuilder b/backend/app/views/secondary_donations/_breakdown.json.jbuilder new file mode 100644 index 00000000..da8f013f --- /dev/null +++ b/backend/app/views/secondary_donations/_breakdown.json.jbuilder @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +json.primaryDonor do + json.amount donation_breakdown[:primary_donor_amount] + json.fraction donation_breakdown[:primary_donor_fraction] +end + +json.secondaryDonors do + json.amount donation_breakdown[:secondary_donors_amount] + json.fraction donation_breakdown[:secondary_donors_fraction] +end diff --git a/backend/app/views/secondary_donations/_secondary_donation.json.jbuilder b/backend/app/views/secondary_donations/_secondary_donation.json.jbuilder index 68cc962c..d9070f15 100644 --- a/backend/app/views/secondary_donations/_secondary_donation.json.jbuilder +++ b/backend/app/views/secondary_donations/_secondary_donation.json.jbuilder @@ -1,16 +1,6 @@ # frozen_string_literal: true json.id secondary_donation.id +json.couponId secondary_donation.coupon_id json.amount secondary_donation.amount - -if secondary_donation.coupon - json.coupon do - json.partial! 'coupons/coupon', coupon: secondary_donation.coupon - end -else - json.coupon nil -end - -json.charity do - json.partial! 'charities/charity', charity: secondary_donation.campaign_charity.charity -end +json.campaignCharityId secondary_donation.campaign_charity_id diff --git a/backend/config/routes.rb b/backend/config/routes.rb index 002e1ec5..570dd885 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -9,7 +9,15 @@ sessions: 'auth/sessions' } - resources :campaigns + resources :campaigns do + collection do + get :admin_index + end + + member do + get 'admin_show' + end + end resources :charities diff --git a/frontend/types/donations.ts b/frontend/types/donations.ts index 544d05de..91dabbc8 100644 --- a/frontend/types/donations.ts +++ b/frontend/types/donations.ts @@ -14,7 +14,7 @@ export type SecondaryDonationData = { id: number; couponId: Nullable; amount: number; - campaignsCharityId: number; + campaignCharityId: number; }; export type SecondaryDonationPostData = WithoutId;