Skip to content

Commit 5a57ee5

Browse files
committed
feat: implement payment gateway
1 parent fe4ae74 commit 5a57ee5

4 files changed

Lines changed: 182 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
module InternalServiceAuthenticatable
4+
extend ActiveSupport::Concern
5+
6+
included do
7+
before_action :authenticate_internal_service!
8+
end
9+
10+
private
11+
12+
def authenticate_internal_service!
13+
token = request.headers['Authorization']&.delete_prefix('Bearer ')
14+
expected = ENV.fetch('INTERNAL_JWT_SECRET', nil)
15+
16+
return if expected.present? && token.present? &&
17+
ActiveSupport::SecurityUtils.secure_compare(token, expected)
18+
19+
render json: { error: 'unauthorized' }, status: :unauthorized
20+
end
21+
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
module Internal
4+
class OrganizationsController < ActionController::API
5+
include InternalServiceAuthenticatable
6+
7+
ALLOWED_TIERS = Constants::Organization::TIERS
8+
ALLOWED_PLANS = Constants::Organization::SUBSCRIPTION_PLANS
9+
ALLOWED_STATUSES = Constants::Organization::SUBSCRIPTION_STATUSES
10+
11+
def update_tier
12+
user = User.find_by(id: params[:user_id])
13+
return render json: { error: 'user not found' }, status: :not_found unless user
14+
15+
org = user.organization
16+
return render json: { error: 'organization not found' }, status: :not_found unless org
17+
18+
tier = params[:tier].to_s
19+
plan = params[:subscription_plan].to_s
20+
status = params[:subscription_status].to_s
21+
22+
unless ALLOWED_TIERS.include?(tier)
23+
return render json: { error: "invalid tier: #{tier}" }, status: :unprocessable_entity
24+
end
25+
26+
unless ALLOWED_PLANS.include?(plan)
27+
return render json: { error: "invalid subscription_plan: #{plan}" }, status: :unprocessable_entity
28+
end
29+
30+
unless ALLOWED_STATUSES.include?(status)
31+
return render json: { error: "invalid subscription_status: #{status}" }, status: :unprocessable_entity
32+
end
33+
34+
org.update!(tier: tier, subscription_plan: plan, subscription_status: status)
35+
36+
render json: {
37+
data: {
38+
id: org.id,
39+
tier: org.tier,
40+
subscription_plan: org.subscription_plan,
41+
subscription_status: org.subscription_status
42+
}
43+
}
44+
rescue ActiveRecord::RecordInvalid => e
45+
render json: { error: e.message }, status: :unprocessable_entity
46+
end
47+
end
48+
end

config/routes.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,9 @@
516516
namespace :api do
517517
get 'inhouse_queues/active', to: '/inhouses/controllers/internal/inhouse_queues#active'
518518
end
519+
520+
# Called by ProPay TierSyncJob when a subscription is activated or cancelled.
521+
patch 'organizations/by_user/:user_id/tier', to: 'organizations#update_tier'
519522
end
520523

521524
require 'sidekiq/web'
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Internal Organizations', type: :request do
6+
let(:organization) { create(:organization, tier: 'tier_3_amateur', subscription_plan: 'free', subscription_status: 'trial') }
7+
let(:user) { create(:user, organization: organization) }
8+
let(:secret) { ENV.fetch('INTERNAL_JWT_SECRET', 'test_internal_secret') }
9+
let(:auth_headers) { { 'Authorization' => "Bearer #{secret}", 'Content-Type' => 'application/json' } }
10+
11+
describe 'PATCH /internal/organizations/by_user/:user_id/tier' do
12+
context 'without Authorization header' do
13+
it 'returns 401' do
14+
patch "/internal/organizations/by_user/#{user.id}/tier",
15+
params: { tier: 'tier_2_semi_pro', subscription_plan: 'semi_pro', subscription_status: 'active' }.to_json,
16+
headers: { 'Content-Type' => 'application/json' }
17+
18+
expect(response).to have_http_status(:unauthorized)
19+
end
20+
end
21+
22+
context 'with wrong secret' do
23+
it 'returns 401' do
24+
patch "/internal/organizations/by_user/#{user.id}/tier",
25+
params: { tier: 'tier_2_semi_pro', subscription_plan: 'semi_pro', subscription_status: 'active' }.to_json,
26+
headers: auth_headers.merge('Authorization' => 'Bearer wrong_secret')
27+
28+
expect(response).to have_http_status(:unauthorized)
29+
end
30+
end
31+
32+
context 'when user does not exist' do
33+
it 'returns 404' do
34+
patch '/internal/organizations/by_user/99999999/tier',
35+
params: { tier: 'tier_2_semi_pro', subscription_plan: 'semi_pro', subscription_status: 'active' }.to_json,
36+
headers: auth_headers
37+
38+
expect(response).to have_http_status(:not_found)
39+
expect(json_response[:error]).to eq('user not found')
40+
end
41+
end
42+
43+
context 'with invalid tier' do
44+
it 'returns 422' do
45+
patch "/internal/organizations/by_user/#{user.id}/tier",
46+
params: { tier: 'tier_9_invalid', subscription_plan: 'semi_pro', subscription_status: 'active' }.to_json,
47+
headers: auth_headers
48+
49+
expect(response).to have_http_status(:unprocessable_entity)
50+
expect(json_response[:error]).to include('invalid tier')
51+
end
52+
end
53+
54+
context 'on subscription activation (pro_monthly)' do
55+
it 'upgrades the organization to tier_2_semi_pro' do
56+
patch "/internal/organizations/by_user/#{user.id}/tier",
57+
params: { tier: 'tier_2_semi_pro', subscription_plan: 'semi_pro', subscription_status: 'active' }.to_json,
58+
headers: auth_headers
59+
60+
expect(response).to have_http_status(:ok)
61+
62+
org = organization.reload
63+
expect(org.tier).to eq('tier_2_semi_pro')
64+
expect(org.subscription_plan).to eq('semi_pro')
65+
expect(org.subscription_status).to eq('active')
66+
end
67+
68+
it 'returns the updated organization data' do
69+
patch "/internal/organizations/by_user/#{user.id}/tier",
70+
params: { tier: 'tier_2_semi_pro', subscription_plan: 'semi_pro', subscription_status: 'active' }.to_json,
71+
headers: auth_headers
72+
73+
data = json_response[:data]
74+
expect(data[:id]).to eq(organization.id)
75+
expect(data[:tier]).to eq('tier_2_semi_pro')
76+
expect(data[:subscription_status]).to eq('active')
77+
end
78+
end
79+
80+
context 'on subscription cancellation' do
81+
before do
82+
organization.update!(tier: 'tier_2_semi_pro', subscription_plan: 'semi_pro', subscription_status: 'active')
83+
end
84+
85+
it 'downgrades the organization to tier_3_amateur' do
86+
patch "/internal/organizations/by_user/#{user.id}/tier",
87+
params: { tier: 'tier_3_amateur', subscription_plan: 'free', subscription_status: 'cancelled' }.to_json,
88+
headers: auth_headers
89+
90+
expect(response).to have_http_status(:ok)
91+
92+
org = organization.reload
93+
expect(org.tier).to eq('tier_3_amateur')
94+
expect(org.subscription_plan).to eq('free')
95+
expect(org.subscription_status).to eq('cancelled')
96+
end
97+
end
98+
99+
context 'on enterprise activation' do
100+
it 'upgrades the organization to tier_1_professional' do
101+
patch "/internal/organizations/by_user/#{user.id}/tier",
102+
params: { tier: 'tier_1_professional', subscription_plan: 'enterprise', subscription_status: 'active' }.to_json,
103+
headers: auth_headers
104+
105+
expect(response).to have_http_status(:ok)
106+
expect(organization.reload.tier).to eq('tier_1_professional')
107+
end
108+
end
109+
end
110+
end

0 commit comments

Comments
 (0)