Skip to content

Commit

Permalink
Accept invite (#447)
Browse files Browse the repository at this point in the history
  • Loading branch information
ansmonjol authored Sep 15, 2022
1 parent 6be245e commit 5924573
Show file tree
Hide file tree
Showing 11 changed files with 457 additions and 16 deletions.
22 changes: 22 additions & 0 deletions app/graphql/mutations/invites/accept.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Mutations
module Invites
class Accept < BaseMutation
graphql_name 'AcceptInvite'
description 'Accepts a new Invite'

argument :email, String, required: true
argument :password, String, required: true
argument :token, String, required: true, description: 'Uniq token of the Invite'

type Types::Payloads::RegisterUserType

def resolve(**args)
result = ::Invites::AcceptService.new.call(**args)

result.success? ? result : result_error(result)
end
end
end
end
1 change: 1 addition & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class MutationType < Types::BaseObject
field :create_customer_wallet_transaction, mutation: Mutations::WalletTransactions::Create

field :create_invite, mutation: Mutations::Invites::Create
field :accept_invite, mutation: Mutations::Invites::Accept
field :revoke_invite, mutation: Mutations::Invites::Revoke
field :revoke_membership, mutation: Mutations::Memberships::Revoke
end
Expand Down
5 changes: 5 additions & 0 deletions app/models/invite.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ def mark_as_revoked!(timestamp = Time.current)
self.revoked_at ||= timestamp
revoked!
end

def mark_as_accepted!(timestamp = Time.current)
self.accepted_at ||= timestamp
accepted!
end
end
24 changes: 24 additions & 0 deletions app/services/invites/accept_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Invites
class AcceptService < BaseService
def call(**args)
invite = Invite.find_by(token: args[:token], status: :pending)
return result.not_found_failure!(resource: 'invite') unless invite

ActiveRecord::Base.transaction do
result = UsersService.new.register_from_invite(
args[:email],
args[:password],
invite.organization_id,
)

invite.recipient = result.membership

invite.mark_as_accepted!

result
end
end
end
end
41 changes: 33 additions & 8 deletions app/services/users_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,8 @@ def register(email, password, organization_name)

ActiveRecord::Base.transaction do
result.organization = Organization.create!(name: organization_name)
result.user.password = password
result.user.save!
result.token = generate_token

result.membership = Membership.create!(
user: result.user,
organization: result.organization,
role: :admin
)
create_user_and_membership(result, password)
end

SegmentIdentifyJob.perform_later(membership_id: "membership/#{result.membership.id}")
Expand All @@ -41,6 +34,20 @@ def register(email, password, organization_name)
result
end

def register_from_invite(email, password, organization_id)
result.user = User.find_or_initialize_by(email: email)

return result.fail!(code: 'user_already_exists') if result.user.id

ActiveRecord::Base.transaction do
result.organization = Organization.find(organization_id)

create_user_and_membership(result, password)
end

result
end

def new_token(user)
result.user = user
result.token = generate_token
Expand All @@ -49,6 +56,24 @@ def new_token(user)

private

def create_user_and_membership(result, password)
ActiveRecord::Base.transaction do
result.user.password = password
result.user.save!

result.token = generate_token

result.membership = Membership.create!(
user: result.user,
organization: result.organization,
)

result
end
rescue ActiveRecord::RecordInvalid => e
result.record_validation_failure!(record: e.record)
end

def generate_token
JWT.encode(payload, ENV['SECRET_KEY_BASE'], 'HS256')
rescue StandardError => e
Expand Down
27 changes: 27 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
"""
Autogenerated input type of AcceptInvite
"""
input AcceptInviteInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
email: String!
password: String!

"""
Uniq token of the Invite
"""
token: String!
}

type AddOn {
amountCents: Int!
amountCurrency: CurrencyEnum!
Expand Down Expand Up @@ -2770,6 +2787,16 @@ enum MembershipStatus {
}

type Mutation {
"""
Accepts a new Invite
"""
acceptInvite(
"""
Parameters for AcceptInvite
"""
input: AcceptInviteInput!
): RegisterUser

"""
Add or update Stripe API keys to the organization
"""
Expand Down
100 changes: 100 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,77 @@
},
"subscriptionType": null,
"types": [
{
"kind": "INPUT_OBJECT",
"name": "AcceptInviteInput",
"description": "Autogenerated input type of AcceptInvite",
"interfaces": null,
"possibleTypes": null,
"fields": null,
"inputFields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "email",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "password",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "token",
"description": "Uniq token of the Invite",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"enumValues": null
},
{
"kind": "OBJECT",
"name": "AddOn",
Expand Down Expand Up @@ -9260,6 +9331,35 @@
],
"possibleTypes": null,
"fields": [
{
"name": "acceptInvite",
"description": "Accepts a new Invite",
"type": {
"kind": "OBJECT",
"name": "RegisterUser",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null,
"args": [
{
"name": "input",
"description": "Parameters for AcceptInvite",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "AcceptInviteInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
]
},
{
"name": "addStripePaymentProvider",
"description": "Add or update Stripe API keys to the organization",
Expand Down
93 changes: 93 additions & 0 deletions spec/graphql/mutations/invites/accept_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Mutations::Invites::Accept, type: :graphql do
let(:membership) { create(:membership) }
let(:organization) { membership.organization }
let(:password) { Faker::Internet.password }

let(:mutation) do
<<~GQL
mutation($input: AcceptInviteInput!) {
acceptInvite(input: $input) {
token
user {
id
email
}
}
}
GQL
end

describe 'Invite revoke mutation' do
context 'with a new user' do
let(:invite) { create(:invite, organization: organization) }

it 'accepts the invite ' do
result = execute_graphql(
current_user: membership.user,
current_organization: organization,
query: mutation,
variables: {
input: {
email: invite.email,
password: password,
token: invite.token,
},
},
)

data = result['data']['acceptInvite']

expect(data['user']['email']).to eq(invite.email)
expect(data['token']).to be_present
end
end

context 'when invite is revoked' do
let(:invite) { create(:invite, organization: organization, status: :revoked) }

it 'returns an error' do
result = execute_graphql(
current_user: membership.user,
current_organization: organization,
query: mutation,
variables: {
input: {
email: invite.email,
password: password,
token: invite.token,
},
},
)

expect(result['errors'].first['extensions']['status']).to eq(404)
expect(result['errors'].first['extensions']['code']).to eq('invite_not_found')
end
end

context 'when invite is already accepted' do
let(:invite) { create(:invite, organization: organization, status: :accepted) }

it 'returns an error' do
result = execute_graphql(
current_user: membership.user,
current_organization: organization,
query: mutation,
variables: {
input: {
email: invite.email,
password: password,
token: invite.token,
},
},
)

expect(result['errors'].first['extensions']['status']).to eq(404)
expect(result['errors'].first['extensions']['code']).to eq('invite_not_found')
end
end
end
end
12 changes: 12 additions & 0 deletions spec/models/invite_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
end
end

describe '#mark_as_accepted' do
let(:invite) { create(:invite) }

it 'accepts the invite with a Time' do
freeze_time do
expect { invite.mark_as_accepted! }
.to change { invite.reload.status }.from('pending').to('accepted')
.and change(invite, :accepted_at).from(nil).to(Time.current)
end
end
end

describe 'Invite email' do
let(:invite) { build(:invite) }

Expand Down
Loading

0 comments on commit 5924573

Please sign in to comment.