Browse Source

Merge pull request #1965 from conectado/council_rework

Solves merge conflict between council_rework and olympia
shamil-gadelshin 4 years ago
parent
commit
198cb0e4fb

+ 3 - 0
runtime-modules/common/src/working_group.rs

@@ -21,8 +21,11 @@ pub enum WorkingGroup {
 
 /// Working group interface to use in the in the pallets with working groups.
 pub trait WorkingGroupIntegration<T: crate::Trait> {
+    /// Validate origin for the worker
     fn ensure_worker_origin(origin: T::Origin, worker_id: &T::ActorId) -> DispatchResult;
 
+    fn get_leader_member_id() -> Option<T::MemberId>;
+
     // TODO: Implement or remove during the Forum refactoring to this interface
     // /// Defines whether the member is the leader of the working group.
     // fn is_working_group_leader(member_id: &T::MemberId) -> bool;

+ 8 - 2
runtime-modules/council/src/mock.rs

@@ -196,7 +196,8 @@ parameter_types! {
     pub const MinimumVotingStake: u64 = 10000;
     pub const MaxSaltLength: u64 = 32; // use some multiple of 8 for ez testing
     pub const VotingLockId: LockIdentifier = *b"referend";
-    pub const MembershipFee: u64 = 100;
+    pub const DefaultMembershipPrice: u64 = 100;
+    pub const DefaultInitialInvitationBalance: u64 = 100;
     pub const MinimumPeriod: u64 = 5;
 }
 
@@ -289,8 +290,9 @@ impl balances::Trait for Runtime {
 
 impl membership::Trait for Runtime {
     type Event = TestEvent;
-    type MembershipFee = MembershipFee;
+    type DefaultMembershipPrice = DefaultMembershipPrice;
     type WorkingGroup = ();
+    type DefaultInitialInvitationBalance = DefaultInitialInvitationBalance;
 }
 
 impl common::working_group::WorkingGroupIntegration<Runtime> for () {
@@ -300,6 +302,10 @@ impl common::working_group::WorkingGroupIntegration<Runtime> for () {
     ) -> DispatchResult {
         unimplemented!();
     }
+
+    fn get_leader_member_id() -> Option<<Runtime as common::Trait>::MemberId> {
+        unimplemented!();
+    }
 }
 
 impl pallet_timestamp::Trait for Runtime {

+ 235 - 61
runtime-modules/membership/src/lib.rs

@@ -35,6 +35,8 @@
 //! - [update_profile_verification](./struct.Module.html#method.update_profile_verification) -
 //! updates member profile verification status.
 //! - [set_referral_cut](./struct.Module.html#method.set_referral_cut) - updates the referral cut.
+//! - [transfer_invites](./struct.Module.html#method.transfer_invites) - transfers the invites
+//! from one member to another.
 //!
 //! [Joystream handbook description](https://joystream.gitbook.io/joystream-handbook/subsystems/membership)
 
@@ -65,10 +67,13 @@ pub trait Trait:
     type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
 
     /// Defines the default membership fee.
-    type MembershipFee: Get<BalanceOf<Self>>;
+    type DefaultMembershipPrice: Get<BalanceOf<Self>>;
 
     /// Working group pallet integration.
     type WorkingGroup: common::working_group::WorkingGroupIntegration<Self>;
+
+    /// Defines the default balance for the invited member.
+    type DefaultInitialInvitationBalance: Get<BalanceOf<Self>>;
 }
 
 // Default user info constraints
@@ -77,6 +82,7 @@ const DEFAULT_MAX_HANDLE_LENGTH: u32 = 40;
 const DEFAULT_MAX_AVATAR_URI_LENGTH: u32 = 1024;
 const DEFAULT_MAX_ABOUT_TEXT_LENGTH: u32 = 2048;
 const DEFAULT_MAX_NAME_LENGTH: u32 = 200;
+pub(crate) const DEFAULT_MEMBER_INVITES_COUNT: u32 = 5;
 
 /// Public membership profile alias.
 pub type Membership<T> = MembershipObject<<T as frame_system::Trait>::AccountId>;
@@ -110,6 +116,9 @@ pub struct MembershipObject<AccountId> {
     /// An indicator that reflects whether the implied real world identity in the profile
     /// corresponds to the true actor behind the membership.
     pub verified: bool,
+
+    /// Defines how many invitations this member has
+    pub invites: u32,
 }
 
 // Contains valid or default user details
@@ -145,12 +154,34 @@ pub struct BuyMembershipParameters<AccountId, MemberId> {
     pub referrer_id: Option<MemberId>,
 }
 
+/// Parameters for the invite_member extrinsic.
+#[derive(Encode, Decode, Default, Clone, PartialEq, Debug)]
+pub struct InviteMembershipParameters<AccountId, MemberId> {
+    /// Inviting member id.
+    pub inviting_member_id: MemberId,
+
+    /// New member root account.
+    pub root_account: AccountId,
+
+    /// New member controller account.
+    pub controller_account: AccountId,
+
+    /// New member user name.
+    pub name: Option<Vec<u8>>,
+
+    /// New member handle.
+    pub handle: Option<Vec<u8>>,
+
+    /// New member avatar URI.
+    pub avatar_uri: Option<Vec<u8>>,
+
+    /// New member 'about' text.
+    pub about: Option<Vec<u8>>,
+}
+
 decl_error! {
     /// Membership module predefined errors
     pub enum Error for Module<T: Trait> {
-        /// New members not allowed.
-        NewMembersNotAllowed,
-
         /// Not enough balance to buy membership.
         NotEnoughBalanceToBuyMembership,
 
@@ -186,6 +217,15 @@ decl_error! {
 
         /// Cannot find a membership for a provided referrer id.
         ReferrerIsNotMember,
+
+        /// Should be a member to receive invites.
+        CannotTransferInvitesForNotMember,
+
+        /// Not enough invites to perform an operation.
+        NotEnoughInvites,
+
+        /// Membership working group leader is not set.
+        WorkingGroupLeaderNotSet,
     }
 }
 
@@ -211,9 +251,6 @@ decl_storage! {
         pub MemberIdByHandle get(fn handles) : map hasher(blake2_128_concat)
             Vec<u8> => T::MemberId;
 
-        /// Is the platform is accepting new members or not.
-        pub NewMembershipsAllowed get(fn new_memberships_allowed) : bool = true;
-
         /// Minimum allowed handle length.
         pub MinHandleLength get(fn min_handle_length) : u32 = DEFAULT_MIN_HANDLE_LENGTH;
 
@@ -231,6 +268,18 @@ decl_storage! {
 
         /// Referral cut to receive during on buying the membership.
         pub ReferralCut get(fn referral_cut) : BalanceOf<T>;
+
+        /// Current membership price.
+        pub MembershipPrice get(fn membership_price) : BalanceOf<T> =
+            T::DefaultMembershipPrice::get();
+
+        /// Initial invitation count for the newly bought membership.
+        pub InitialInvitationCount get(fn initial_invitation_count) : u32  =
+            DEFAULT_MEMBER_INVITES_COUNT;
+
+        /// Initial invitation balance for the invited member.
+        pub InitialInvitationBalance get(fn initial_invitation_balance) : BalanceOf<T> =
+            T::DefaultInitialInvitationBalance::get();
     }
     add_extra_genesis {
         config(members) : Vec<genesis::Member<T::MemberId, T::AccountId>>;
@@ -247,6 +296,7 @@ decl_storage! {
                     &member.root_account,
                     &member.controller_account,
                     &checked_user_info,
+                    Zero::zero(),
                 ).expect("Importing Member Failed");
 
                 // ensure imported member id matches assigned id
@@ -266,6 +316,11 @@ decl_event! {
         MemberAccountsUpdated(MemberId),
         MemberVerificationStatusUpdated(MemberId, bool),
         ReferralCutUpdated(Balance),
+        InvitesTransferred(MemberId, MemberId, u32),
+        MembershipPriceUpdated(Balance),
+        InitialInvitationBalanceUpdated(Balance),
+        LeaderInvitationQuotaUpdated(u32),
+        InitialInvitationCountUpdated(u32),
     }
 }
 
@@ -273,9 +328,6 @@ decl_module! {
     pub struct Module<T: Trait> for enum Call where origin: T::Origin {
         fn deposit_event() = default;
 
-        /// Exports const - membership fee.
-        const MembershipFee: BalanceOf<T> = T::MembershipFee::get();
-
         /// Non-members can buy membership.
         #[weight = 10_000_000] // TODO: adjust weight
         pub fn buy_membership(
@@ -284,12 +336,9 @@ decl_module! {
         ) {
             let who = ensure_signed(origin)?;
 
-            // Make sure we are accepting new memberships.
-            ensure!(Self::new_memberships_allowed(), Error::<T>::NewMembersNotAllowed);
-
-            let fee = T::MembershipFee::get();
+            let fee = Self::membership_price();
 
-            // Ensure enough free balance to cover terms fees.
+            // Ensure enough free balance to cover membership fee.
             ensure!(
                 balances::Module::<T>::usable_balance(&who) >= fee,
                 Error::<T>::NotEnoughBalanceToBuyMembership
@@ -318,6 +367,7 @@ decl_module! {
                 &params.root_account,
                 &params.controller_account,
                 &user_info,
+                Self::initial_invitation_count(),
             )?;
 
             // Collect membership fee (just burn it).
@@ -475,10 +525,7 @@ decl_module! {
 
         /// Updates membership referral cut. Requires root origin.
         #[weight = 10_000_000] // TODO: adjust weight
-        pub fn set_referral_cut(
-            origin,
-            value: BalanceOf<T>
-        ) {
+        pub fn set_referral_cut(origin, value: BalanceOf<T>) {
             ensure_root(origin)?;
 
             //
@@ -489,6 +536,154 @@ decl_module! {
 
             Self::deposit_event(RawEvent::ReferralCutUpdated(value));
         }
+
+        /// Transfers invites from one member to another.
+        #[weight = 10_000_000] // TODO: adjust weight
+        pub fn transfer_invites(
+            origin,
+            source_member_id: T::MemberId,
+            target_member_id: T::MemberId,
+            number_of_invites: u32
+        ) {
+            Self::ensure_member_controller_account_signed(origin, &source_member_id)?;
+
+            let source_membership = Self::ensure_membership(source_member_id)?;
+            Self::ensure_membership_with_error(
+                target_member_id,
+                Error::<T>::CannotTransferInvitesForNotMember
+            )?;
+
+            ensure!(source_membership.invites >= number_of_invites, Error::<T>::NotEnoughInvites);
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Decrease source member invite number.
+            <MembershipById<T>>::mutate(&source_member_id, |membership| {
+                membership.invites = membership.invites.saturating_sub(number_of_invites);
+            });
+
+            // Increase target member invite number.
+            <MembershipById<T>>::mutate(&target_member_id, |membership| {
+                membership.invites = membership.invites.saturating_add(number_of_invites);
+            });
+
+            Self::deposit_event(RawEvent::InvitesTransferred(
+                source_member_id,
+                target_member_id,
+                number_of_invites
+            ));
+        }
+
+        /// Invite a new member.
+        #[weight = 10_000_000] // TODO: adjust weight
+        pub fn invite_member(
+            origin,
+            params: InviteMembershipParameters<T::AccountId, T::MemberId>
+        ) {
+            let membership = Self::ensure_member_controller_account_signed(
+                origin,
+                &params.inviting_member_id
+            )?;
+
+            ensure!(membership.invites > Zero::zero(), Error::<T>::NotEnoughInvites);
+
+            // Verify user parameters.
+            let user_info = Self::check_user_registration_info(
+                params.name,
+                params.handle,
+                params.avatar_uri,
+                params.about
+            )?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            let member_id = Self::insert_member(
+                &params.root_account,
+                &params.controller_account,
+                &user_info,
+                Zero::zero(),
+            )?;
+
+            // Save the updated profile.
+            <MembershipById<T>>::mutate(&member_id, |membership| {
+                membership.invites = membership.invites.saturating_sub(1);
+            });
+
+            // Fire the event.
+            Self::deposit_event(RawEvent::MemberRegistered(member_id));
+        }
+
+        /// Updates membership price. Requires root origin.
+        #[weight = 10_000_000] // TODO: adjust weight
+        pub fn set_membership_price(origin, new_price: BalanceOf<T>) {
+            ensure_root(origin)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            <MembershipPrice<T>>::put(new_price);
+
+            Self::deposit_event(RawEvent::MembershipPriceUpdated(new_price));
+        }
+
+        /// Updates leader invitation quota. Requires root origin.
+        #[weight = 10_000_000] // TODO: adjust weight
+        pub fn set_leader_invitation_quota(origin, invitation_quota: u32) {
+            ensure_root(origin)?;
+
+            let leader_member_id = T::WorkingGroup::get_leader_member_id();
+
+            if let Some(member_id) = leader_member_id{
+                Self::ensure_membership(member_id)?;
+            }
+
+            ensure!(leader_member_id.is_some(), Error::<T>::WorkingGroupLeaderNotSet);
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            if let Some(member_id) = leader_member_id{
+                <MembershipById<T>>::mutate(&member_id, |membership| {
+                        membership.invites = invitation_quota;
+                });
+
+                Self::deposit_event(RawEvent::LeaderInvitationQuotaUpdated(invitation_quota));
+            }
+        }
+
+        /// Updates initial invitation balance for a invited member. Requires root origin.
+        #[weight = 10_000_000] // TODO: adjust weight
+        pub fn set_initial_invitation_balance(origin, new_initial_balance: BalanceOf<T>) {
+            ensure_root(origin)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            <InitialInvitationBalance<T>>::put(new_initial_balance);
+
+            Self::deposit_event(RawEvent::InitialInvitationBalanceUpdated(new_initial_balance));
+        }
+
+        /// Updates initial invitation count for a member. Requires root origin.
+        #[weight = 10_000_000] // TODO: adjust weight
+        pub fn set_initial_invitation_count(origin, new_invitation_count: u32) {
+            ensure_root(origin)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            InitialInvitationCount::put(new_invitation_count);
+
+            Self::deposit_event(RawEvent::InitialInvitationCountUpdated(new_invitation_count));
+        }
     }
 }
 
@@ -510,24 +705,6 @@ impl<T: Trait> Module<T> {
         }
     }
 
-    /// Ensure that given member has given account as the controller account
-    pub fn ensure_is_controller_account_for_member(
-        member_id: &T::MemberId,
-        account: &T::AccountId,
-    ) -> Result<Membership<T>, Error<T>> {
-        if MembershipById::<T>::contains_key(member_id) {
-            let membership = MembershipById::<T>::get(member_id);
-
-            if membership.controller_account == *account {
-                Ok(membership)
-            } else {
-                Err(Error::<T>::ControllerAccountRequired)
-            }
-        } else {
-            Err(Error::<T>::MemberProfileNotFound)
-        }
-    }
-
     /// Returns true if account is either a member's root or controller account
     pub fn is_member_account(who: &T::AccountId) -> bool {
         <MemberIdsByRootAccountId<T>>::contains_key(who)
@@ -591,6 +768,7 @@ impl<T: Trait> Module<T> {
     ) -> Result<ValidatedUserInfo, Error<T>> {
         // Handle is required during registration
         let handle = handle.ok_or(Error::<T>::HandleMustBeProvidedDuringRegistration)?;
+        Self::ensure_unique_handle(&handle)?;
         Self::validate_handle(&handle)?;
 
         let about = Self::validate_text(&about.unwrap_or_default());
@@ -607,14 +785,13 @@ impl<T: Trait> Module<T> {
         })
     }
 
-    // Inserts a member using a validated information. Sets handle, accounts caches.
+    // Inserts a member using a validated information. Sets handle, accounts caches, etc..
     fn insert_member(
         root_account: &T::AccountId,
         controller_account: &T::AccountId,
         user_info: &ValidatedUserInfo,
+        allowed_invites: u32,
     ) -> Result<T::MemberId, Error<T>> {
-        Self::ensure_unique_handle(&user_info.handle)?;
-
         let new_member_id = Self::members_created();
 
         let membership: Membership<T> = MembershipObject {
@@ -625,6 +802,7 @@ impl<T: Trait> Module<T> {
             root_account: root_account.clone(),
             controller_account: controller_account.clone(),
             verified: false,
+            invites: allowed_invites,
         };
 
         <MemberIdsByRootAccountId<T>>::mutate(root_account, |ids| {
@@ -641,44 +819,40 @@ impl<T: Trait> Module<T> {
         Ok(new_member_id)
     }
 
-    /// Ensure origin corresponds to the controller account of the member.
-    pub fn ensure_member_controller_account_signed(
+    // Ensure origin corresponds to the controller account of the member.
+    fn ensure_member_controller_account_signed(
         origin: T::Origin,
         member_id: &T::MemberId,
-    ) -> Result<T::AccountId, Error<T>> {
+    ) -> Result<Membership<T>, Error<T>> {
         // Ensure transaction is signed.
-        let signer_account = ensure_signed(origin).map_err(|_| Error::<T>::UnsignedOrigin)?;
+        let signer_account_id = ensure_signed(origin).map_err(|_| Error::<T>::UnsignedOrigin)?;
 
-        // Ensure member exists
-        let membership = Self::ensure_membership(*member_id)?;
+        Self::ensure_is_controller_account_for_member(member_id, &signer_account_id)
+    }
 
+    /// Ensure that given member has given account as the controller account
+    pub fn ensure_is_controller_account_for_member(
+        member_id: &T::MemberId,
+        account: &T::AccountId,
+    ) -> Result<Membership<T>, Error<T>> {
         ensure!(
-            membership.controller_account == signer_account,
-            Error::<T>::ControllerAccountRequired
+            MembershipById::<T>::contains_key(member_id),
+            Error::<T>::MemberProfileNotFound
         );
 
-        Ok(signer_account)
-    }
-
-    /// Validates that a member has the controller account.
-    pub fn ensure_member_controller_account(
-        signer_account: &T::AccountId,
-        member_id: &T::MemberId,
-    ) -> Result<(), Error<T>> {
-        // Ensure member exists
-        let membership = Self::ensure_membership(*member_id)?;
+        let membership = MembershipById::<T>::get(member_id);
 
         ensure!(
-            membership.controller_account == *signer_account,
+            membership.controller_account == *account,
             Error::<T>::ControllerAccountRequired
         );
 
-        Ok(())
+        Ok(membership)
     }
 
     // Calculate current referral bonus. It minimum between membership fee and referral cut.
     pub(crate) fn get_referral_bonus() -> BalanceOf<T> {
-        let membership_fee = T::MembershipFee::get();
+        let membership_fee = Self::membership_price();
         let referral_cut = Self::referral_cut();
 
         membership_fee.min(referral_cut)

+ 291 - 1
runtime-modules/membership/src/tests/fixtures.rs

@@ -1,5 +1,5 @@
 use super::mock::*;
-use crate::BuyMembershipParameters;
+use crate::{BuyMembershipParameters, InviteMembershipParameters};
 use frame_support::dispatch::DispatchResult;
 use frame_support::traits::{OnFinalize, OnInitialize};
 use frame_support::StorageMap;
@@ -77,6 +77,8 @@ pub fn get_bob_info() -> TestUserInfo {
 
 pub const ALICE_ACCOUNT_ID: u64 = 1;
 pub const BOB_ACCOUNT_ID: u64 = 2;
+pub const ALICE_MEMBER_ID: u64 = 0;
+pub const BOB_MEMBER_ID: u64 = 1;
 
 pub fn buy_default_membership_as_alice() -> DispatchResult {
     let info = get_alice_info();
@@ -263,3 +265,291 @@ impl SetReferralCutFixture {
         Self { origin, ..self }
     }
 }
+
+pub struct TransferInvitesFixture {
+    pub origin: RawOrigin<u64>,
+    pub source_member_id: u64,
+    pub target_member_id: u64,
+    pub invites: u32,
+}
+
+impl Default for TransferInvitesFixture {
+    fn default() -> Self {
+        Self {
+            origin: RawOrigin::Signed(ALICE_ACCOUNT_ID),
+            source_member_id: ALICE_MEMBER_ID,
+            target_member_id: BOB_MEMBER_ID,
+            invites: 3,
+        }
+    }
+}
+
+impl TransferInvitesFixture {
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result = Membership::transfer_invites(
+            self.origin.clone().into(),
+            self.source_member_id,
+            self.target_member_id,
+            self.invites,
+        );
+
+        assert_eq!(expected_result, actual_result);
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_source_member_id(self, source_member_id: u64) -> Self {
+        Self {
+            source_member_id,
+            ..self
+        }
+    }
+
+    pub fn with_target_member_id(self, target_member_id: u64) -> Self {
+        Self {
+            target_member_id,
+            ..self
+        }
+    }
+
+    pub fn with_invites_number(self, invites: u32) -> Self {
+        Self { invites, ..self }
+    }
+}
+
+pub struct InviteMembershipFixture {
+    pub member_id: u64,
+    pub origin: RawOrigin<u64>,
+    pub root_account: u64,
+    pub controller_account: u64,
+    pub name: Option<Vec<u8>>,
+    pub handle: Option<Vec<u8>>,
+    pub avatar_uri: Option<Vec<u8>>,
+    pub about: Option<Vec<u8>>,
+}
+
+impl Default for InviteMembershipFixture {
+    fn default() -> Self {
+        let bob = get_bob_info();
+        Self {
+            member_id: ALICE_MEMBER_ID,
+            origin: RawOrigin::Signed(ALICE_ACCOUNT_ID),
+            root_account: BOB_ACCOUNT_ID,
+            controller_account: BOB_ACCOUNT_ID,
+            name: bob.name,
+            handle: bob.handle,
+            avatar_uri: bob.avatar_uri,
+            about: bob.about,
+        }
+    }
+}
+
+impl InviteMembershipFixture {
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let params = InviteMembershipParameters {
+            inviting_member_id: self.member_id.clone(),
+            root_account: self.root_account.clone(),
+            controller_account: self.controller_account.clone(),
+            name: self.name.clone(),
+            handle: self.handle.clone(),
+            avatar_uri: self.avatar_uri.clone(),
+            about: self.about.clone(),
+        };
+
+        let actual_result = Membership::invite_member(self.origin.clone().into(), params);
+
+        assert_eq!(expected_result, actual_result);
+    }
+
+    pub fn with_member_id(self, member_id: u64) -> Self {
+        Self { member_id, ..self }
+    }
+
+    pub fn with_name(self, name: Vec<u8>) -> Self {
+        Self {
+            name: Some(name),
+            ..self
+        }
+    }
+
+    pub fn with_avatar(self, avatar_uri: Vec<u8>) -> Self {
+        Self {
+            avatar_uri: Some(avatar_uri),
+            ..self
+        }
+    }
+
+    pub fn with_handle(self, handle: Vec<u8>) -> Self {
+        Self {
+            handle: Some(handle),
+            ..self
+        }
+    }
+
+    pub fn with_empty_handle(self) -> Self {
+        Self {
+            handle: None,
+            ..self
+        }
+    }
+
+    pub fn with_accounts(self, account_id: u64) -> Self {
+        Self {
+            root_account: account_id,
+            controller_account: account_id,
+            ..self
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+}
+
+pub struct SetMembershipPriceFixture {
+    pub origin: RawOrigin<u64>,
+    pub price: u64,
+}
+
+pub const DEFAULT_MEMBERSHIP_PRICE: u64 = 100;
+
+impl Default for SetMembershipPriceFixture {
+    fn default() -> Self {
+        Self {
+            origin: RawOrigin::Root,
+            price: DEFAULT_MEMBERSHIP_PRICE,
+        }
+    }
+}
+
+impl SetMembershipPriceFixture {
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result =
+            Membership::set_membership_price(self.origin.clone().into(), self.price);
+
+        assert_eq!(expected_result, actual_result);
+
+        if actual_result.is_ok() {
+            assert_eq!(Membership::membership_price(), self.price);
+        }
+    }
+
+    pub fn with_price(self, price: u64) -> Self {
+        Self { price, ..self }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+}
+
+pub struct SetLeaderInvitationQuotaFixture {
+    pub origin: RawOrigin<u64>,
+    pub quota: u32,
+}
+
+pub const DEFAULT_LEADER_INVITATION_QUOTA: u32 = 100;
+
+impl Default for SetLeaderInvitationQuotaFixture {
+    fn default() -> Self {
+        Self {
+            origin: RawOrigin::Root,
+            quota: DEFAULT_LEADER_INVITATION_QUOTA,
+        }
+    }
+}
+
+impl SetLeaderInvitationQuotaFixture {
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result =
+            Membership::set_leader_invitation_quota(self.origin.clone().into(), self.quota);
+
+        assert_eq!(expected_result, actual_result);
+
+        if actual_result.is_ok() {
+            assert_eq!(Membership::membership(ALICE_MEMBER_ID).invites, self.quota);
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+}
+
+pub struct SetInitialInvitationBalanceFixture {
+    pub origin: RawOrigin<u64>,
+    pub new_initial_balance: u64,
+}
+
+pub const DEFAULT_INITIAL_INVITATION_BALANCE: u64 = 200;
+
+impl Default for SetInitialInvitationBalanceFixture {
+    fn default() -> Self {
+        Self {
+            origin: RawOrigin::Root,
+            new_initial_balance: DEFAULT_INITIAL_INVITATION_BALANCE,
+        }
+    }
+}
+
+impl SetInitialInvitationBalanceFixture {
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result = Membership::set_initial_invitation_balance(
+            self.origin.clone().into(),
+            self.new_initial_balance,
+        );
+
+        assert_eq!(expected_result, actual_result);
+
+        if actual_result.is_ok() {
+            assert_eq!(
+                Membership::initial_invitation_balance(),
+                self.new_initial_balance
+            );
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+}
+
+pub struct SetInitialInvitationCountFixture {
+    pub origin: RawOrigin<u64>,
+    pub new_invitation_count: u32,
+}
+
+pub const DEFAULT_INITIAL_INVITATION_COUNT: u32 = 5;
+
+impl Default for SetInitialInvitationCountFixture {
+    fn default() -> Self {
+        Self {
+            origin: RawOrigin::Root,
+            new_invitation_count: DEFAULT_INITIAL_INVITATION_COUNT,
+        }
+    }
+}
+
+impl SetInitialInvitationCountFixture {
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result = Membership::set_initial_invitation_count(
+            self.origin.clone().into(),
+            self.new_invitation_count,
+        );
+
+        assert_eq!(expected_result, actual_result);
+
+        if actual_result.is_ok() {
+            assert_eq!(
+                Membership::initial_invitation_count(),
+                self.new_invitation_count
+            );
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+}

+ 9 - 2
runtime-modules/membership/src/tests/mock.rs

@@ -7,6 +7,7 @@ use staking_handler::LockComparator;
 pub use frame_support::traits::{Currency, LockIdentifier};
 use frame_support::{impl_outer_event, impl_outer_origin, parameter_types};
 
+use crate::tests::fixtures::ALICE_MEMBER_ID;
 pub use frame_system;
 use frame_system::RawOrigin;
 use sp_core::H256;
@@ -83,7 +84,7 @@ impl pallet_timestamp::Trait for Test {
 
 parameter_types! {
     pub const ExistentialDeposit: u32 = 0;
-    pub const MembershipFee: u64 = 100;
+    pub const DefaultMembershipPrice: u64 = 100;
 }
 
 impl balances::Trait for Test {
@@ -104,6 +105,7 @@ impl common::Trait for Test {
 parameter_types! {
     pub const MaxWorkerNumberLimit: u32 = 3;
     pub const LockId: LockIdentifier = [9; 8];
+    pub const DefaultInitialInvitationBalance: u64 = 100;
 }
 
 impl working_group::Trait<MembershipWorkingGroupInstance> for Test {
@@ -231,8 +233,9 @@ impl common::origin::ActorOriginValidator<Origin, u64, u64> for () {
 
 impl Trait for Test {
     type Event = TestEvent;
-    type MembershipFee = MembershipFee;
+    type DefaultMembershipPrice = DefaultMembershipPrice;
     type WorkingGroup = ();
+    type DefaultInitialInvitationBalance = DefaultInitialInvitationBalance;
 }
 
 impl common::working_group::WorkingGroupIntegration<Test> for () {
@@ -253,6 +256,10 @@ impl common::working_group::WorkingGroupIntegration<Test> for () {
             Err(DispatchError::BadOrigin)
         }
     }
+
+    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+        Some(ALICE_MEMBER_ID)
+    }
 }
 
 pub struct TestExternalitiesBuilder<T: Trait> {

+ 373 - 43
runtime-modules/membership/src/tests/mod.rs

@@ -18,7 +18,7 @@ fn buy_membership_succeeds() {
         let starting_block = 1;
         run_to_block(starting_block);
 
-        let initial_balance = MembershipFee::get();
+        let initial_balance = DefaultMembershipPrice::get();
         set_alice_free_balance(initial_balance);
 
         let next_member_id = Membership::members_created();
@@ -34,6 +34,7 @@ fn buy_membership_succeeds() {
         assert_eq!(Some(profile.handle), get_alice_info().handle);
         assert_eq!(Some(profile.avatar_uri), get_alice_info().avatar_uri);
         assert_eq!(Some(profile.about), get_alice_info().about);
+        assert_eq!(profile.invites, crate::DEFAULT_MEMBER_INVITES_COUNT);
 
         // controller account initially set to primary account
         assert_eq!(profile.controller_account, ALICE_ACCOUNT_ID);
@@ -49,7 +50,7 @@ fn buy_membership_succeeds() {
 #[test]
 fn buy_membership_fails_without_enough_balance() {
     build_test_externalities().execute_with(|| {
-        let initial_balance = MembershipFee::get() - 1;
+        let initial_balance = DefaultMembershipPrice::get() - 1;
         set_alice_free_balance(initial_balance);
 
         assert_dispatch_error_message(
@@ -62,7 +63,7 @@ fn buy_membership_fails_without_enough_balance() {
 #[test]
 fn buy_membership_fails_without_enough_balance_with_locked_balance() {
     build_test_externalities().execute_with(|| {
-        let initial_balance = MembershipFee::get();
+        let initial_balance = DefaultMembershipPrice::get();
         let lock_id = LockIdentifier::default();
         Balances::set_lock(lock_id, &ALICE_ACCOUNT_ID, 1, WithdrawReasons::all());
         set_alice_free_balance(initial_balance);
@@ -74,25 +75,10 @@ fn buy_membership_fails_without_enough_balance_with_locked_balance() {
     });
 }
 
-#[test]
-fn new_memberships_allowed_flag_works() {
-    build_test_externalities().execute_with(|| {
-        let initial_balance = MembershipFee::get() + 1;
-        set_alice_free_balance(initial_balance);
-
-        crate::NewMembershipsAllowed::put(false);
-
-        assert_dispatch_error_message(
-            buy_default_membership_as_alice(),
-            Err(Error::<Test>::NewMembersNotAllowed.into()),
-        );
-    });
-}
-
 #[test]
 fn buy_membership_fails_with_non_unique_handle() {
     build_test_externalities().execute_with(|| {
-        let initial_balance = MembershipFee::get();
+        let initial_balance = DefaultMembershipPrice::get();
         set_alice_free_balance(initial_balance);
 
         // alice's handle already taken
@@ -112,7 +98,7 @@ fn update_profile_succeeds() {
         let starting_block = 1;
         run_to_block(starting_block);
 
-        let initial_balance = MembershipFee::get();
+        let initial_balance = DefaultMembershipPrice::get();
         set_alice_free_balance(initial_balance);
 
         let next_member_id = Membership::members_created();
@@ -146,7 +132,7 @@ fn update_profile_succeeds() {
 #[test]
 fn update_profile_has_no_effect_on_empty_parameters() {
     build_test_externalities().execute_with(|| {
-        let initial_balance = MembershipFee::get();
+        let initial_balance = DefaultMembershipPrice::get();
         set_alice_free_balance(initial_balance);
 
         let next_member_id = Membership::members_created();
@@ -176,8 +162,7 @@ fn update_profile_has_no_effect_on_empty_parameters() {
 
 #[test]
 fn update_profile_accounts_succeeds() {
-    let member_id = 0u64;
-    let initial_members = [(member_id, ALICE_ACCOUNT_ID)];
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
 
     build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
         let starting_block = 1;
@@ -187,56 +172,57 @@ fn update_profile_accounts_succeeds() {
 
         assert_ok!(Membership::update_accounts(
             Origin::signed(ALICE_ACCOUNT_ID),
-            member_id,
+            ALICE_MEMBER_ID,
             Some(ALICE_NEW_ACCOUNT_ID),
             Some(ALICE_NEW_ACCOUNT_ID),
         ));
 
-        let profile = get_membership_by_id(member_id);
+        let profile = get_membership_by_id(ALICE_MEMBER_ID);
 
         assert_eq!(profile.controller_account, ALICE_NEW_ACCOUNT_ID);
         assert_eq!(
             <crate::MemberIdsByControllerAccountId<Test>>::get(&ALICE_NEW_ACCOUNT_ID),
-            vec![member_id]
+            vec![ALICE_MEMBER_ID]
         );
         assert!(<crate::MemberIdsByControllerAccountId<Test>>::get(&ALICE_ACCOUNT_ID).is_empty());
 
         assert_eq!(profile.root_account, ALICE_NEW_ACCOUNT_ID);
         assert_eq!(
             <crate::MemberIdsByRootAccountId<Test>>::get(&ALICE_NEW_ACCOUNT_ID),
-            vec![member_id]
+            vec![ALICE_MEMBER_ID]
         );
         assert!(<crate::MemberIdsByRootAccountId<Test>>::get(&ALICE_ACCOUNT_ID).is_empty());
 
-        EventFixture::assert_last_crate_event(Event::<Test>::MemberAccountsUpdated(member_id));
+        EventFixture::assert_last_crate_event(Event::<Test>::MemberAccountsUpdated(
+            ALICE_MEMBER_ID,
+        ));
     });
 }
 
 #[test]
 fn update_accounts_has_effect_on_empty_account_parameters() {
-    let member_id = 0u64;
-    let initial_members = [(member_id, ALICE_ACCOUNT_ID)];
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
 
     build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
         assert_ok!(Membership::update_accounts(
             Origin::signed(ALICE_ACCOUNT_ID),
-            member_id,
+            ALICE_MEMBER_ID,
             None,
             None,
         ));
 
-        let profile = get_membership_by_id(member_id);
+        let profile = get_membership_by_id(ALICE_MEMBER_ID);
 
         assert_eq!(profile.controller_account, ALICE_ACCOUNT_ID);
         assert_eq!(
             <crate::MemberIdsByControllerAccountId<Test>>::get(&ALICE_ACCOUNT_ID),
-            vec![member_id]
+            vec![ALICE_MEMBER_ID]
         );
 
         assert_eq!(profile.root_account, ALICE_ACCOUNT_ID);
         assert_eq!(
             <crate::MemberIdsByRootAccountId<Test>>::get(&ALICE_ACCOUNT_ID),
-            vec![member_id]
+            vec![ALICE_MEMBER_ID]
         );
     });
 }
@@ -247,7 +233,7 @@ fn update_verification_status_succeeds() {
         let starting_block = 1;
         run_to_block(starting_block);
 
-        let initial_balance = MembershipFee::get();
+        let initial_balance = DefaultMembershipPrice::get();
         set_alice_free_balance(initial_balance);
 
         let next_member_id = Membership::members_created();
@@ -290,7 +276,7 @@ fn update_verification_status_fails_with_invalid_member_id() {
 #[test]
 fn update_verification_status_fails_with_invalid_worker_id() {
     build_test_externalities().execute_with(|| {
-        let initial_balance = MembershipFee::get();
+        let initial_balance = DefaultMembershipPrice::get();
         set_alice_free_balance(initial_balance);
 
         let next_member_id = Membership::members_created();
@@ -312,7 +298,7 @@ fn update_verification_status_fails_with_invalid_worker_id() {
 #[test]
 fn buy_membership_fails_with_invalid_name() {
     build_test_externalities().execute_with(|| {
-        let initial_balance = MembershipFee::get();
+        let initial_balance = DefaultMembershipPrice::get();
         set_alice_free_balance(initial_balance);
 
         let name: [u8; 500] = [1; 500];
@@ -326,7 +312,7 @@ fn buy_membership_fails_with_invalid_name() {
 #[test]
 fn buy_membership_fails_with_non_member_referrer_id() {
     build_test_externalities().execute_with(|| {
-        let initial_balance = MembershipFee::get();
+        let initial_balance = DefaultMembershipPrice::get();
         set_alice_free_balance(initial_balance);
 
         let invalid_member_id = 111;
@@ -340,8 +326,7 @@ fn buy_membership_fails_with_non_member_referrer_id() {
 
 #[test]
 fn buy_membership_with_referral_cut_succeeds() {
-    let member_id = 0u64;
-    let initial_members = [(member_id, ALICE_ACCOUNT_ID)];
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
 
     build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
         let initial_balance = 10000;
@@ -351,7 +336,7 @@ fn buy_membership_with_referral_cut_succeeds() {
             .with_handle(b"bobs_handle".to_vec())
             .with_accounts(BOB_ACCOUNT_ID)
             .with_origin(RawOrigin::Signed(BOB_ACCOUNT_ID))
-            .with_referrer_id(member_id);
+            .with_referrer_id(ALICE_MEMBER_ID);
 
         buy_membership_fixture.call_and_assert(Ok(()));
 
@@ -360,7 +345,7 @@ fn buy_membership_with_referral_cut_succeeds() {
         assert_eq!(Balances::usable_balance(&ALICE_ACCOUNT_ID), referral_cut);
         assert_eq!(
             Balances::usable_balance(&BOB_ACCOUNT_ID),
-            initial_balance - MembershipFee::get()
+            initial_balance - DefaultMembershipPrice::get()
         );
     });
 }
@@ -369,7 +354,7 @@ fn buy_membership_with_referral_cut_succeeds() {
 fn referral_bonus_calculated_successfully() {
     build_test_externalities().execute_with(|| {
         // it should take minimum of the referral cut and membership fee
-        let membership_fee = MembershipFee::get();
+        let membership_fee = DefaultMembershipPrice::get();
         let diff = 10;
 
         let referral_cut = membership_fee.saturating_sub(diff);
@@ -404,3 +389,348 @@ fn set_referral_fails_with_invalid_origin() {
             .call_and_assert(Err(DispatchError::BadOrigin));
     });
 }
+
+#[test]
+fn transfer_invites_succeeds() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 10000;
+        increase_total_balance_issuance_using_account_id(BOB_ACCOUNT_ID, initial_balance);
+
+        BuyMembershipFixture::default()
+            .with_handle(b"bobs_handle".to_vec())
+            .with_accounts(BOB_ACCOUNT_ID)
+            .with_origin(RawOrigin::Signed(BOB_ACCOUNT_ID))
+            .call_and_assert(Ok(()));
+
+        let bob_member_id = 1;
+
+        let tranfer_invites_fixture = TransferInvitesFixture::default()
+            .with_origin(RawOrigin::Signed(BOB_ACCOUNT_ID))
+            .with_source_member_id(bob_member_id)
+            .with_target_member_id(ALICE_MEMBER_ID);
+
+        tranfer_invites_fixture.call_and_assert(Ok(()));
+
+        let alice = Membership::membership(ALICE_MEMBER_ID);
+        let bob = Membership::membership(bob_member_id);
+
+        assert_eq!(alice.invites, tranfer_invites_fixture.invites);
+        assert_eq!(
+            bob.invites,
+            crate::DEFAULT_MEMBER_INVITES_COUNT - tranfer_invites_fixture.invites
+        );
+
+        EventFixture::assert_last_crate_event(Event::<Test>::InvitesTransferred(
+            bob_member_id,
+            ALICE_MEMBER_ID,
+            tranfer_invites_fixture.invites,
+        ));
+    });
+}
+
+#[test]
+fn transfer_invites_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        TransferInvitesFixture::default()
+            .with_origin(RawOrigin::None)
+            .call_and_assert(Err(Error::<Test>::UnsignedOrigin.into()));
+    });
+}
+
+#[test]
+fn transfer_invites_fails_with_source_member_id() {
+    build_test_externalities().execute_with(|| {
+        TransferInvitesFixture::default()
+            .call_and_assert(Err(Error::<Test>::MemberProfileNotFound.into()));
+    });
+}
+
+#[test]
+fn transfer_invites_fails_with_target_member_id() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        TransferInvitesFixture::default()
+            .call_and_assert(Err(Error::<Test>::CannotTransferInvitesForNotMember.into()));
+    });
+}
+
+#[test]
+fn transfer_invites_fails_when_not_enough_invites() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 10000;
+        increase_total_balance_issuance_using_account_id(BOB_ACCOUNT_ID, initial_balance);
+
+        BuyMembershipFixture::default()
+            .with_handle(b"bobs_handle".to_vec())
+            .with_accounts(BOB_ACCOUNT_ID)
+            .with_origin(RawOrigin::Signed(BOB_ACCOUNT_ID))
+            .call_and_assert(Ok(()));
+
+        let bob_member_id = 1;
+
+        let invalid_invites_number = 100;
+        let tranfer_invites_fixture = TransferInvitesFixture::default()
+            .with_origin(RawOrigin::Signed(BOB_ACCOUNT_ID))
+            .with_source_member_id(bob_member_id)
+            .with_target_member_id(ALICE_MEMBER_ID)
+            .with_invites_number(invalid_invites_number);
+
+        tranfer_invites_fixture.call_and_assert(Err(Error::<Test>::NotEnoughInvites.into()));
+    });
+}
+
+#[test]
+fn invite_member_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = DefaultMembershipPrice::get();
+        set_alice_free_balance(initial_balance);
+
+        assert_ok!(buy_default_membership_as_alice());
+        let bob_member_id = Membership::members_created();
+
+        InviteMembershipFixture::default().call_and_assert(Ok(()));
+
+        let profile = get_membership_by_id(bob_member_id);
+
+        let bob = get_bob_info();
+        assert_eq!(Some(profile.name), bob.name);
+        assert_eq!(Some(profile.handle), bob.handle);
+        assert_eq!(Some(profile.avatar_uri), bob.avatar_uri);
+        assert_eq!(Some(profile.about), bob.about);
+        assert_eq!(profile.invites, 0);
+
+        // controller account initially set to primary account
+        assert_eq!(profile.controller_account, BOB_ACCOUNT_ID);
+        assert_eq!(
+            <crate::MemberIdsByControllerAccountId<Test>>::get(BOB_ACCOUNT_ID),
+            vec![bob_member_id]
+        );
+
+        EventFixture::assert_last_crate_event(Event::<Test>::MemberRegistered(bob_member_id));
+    });
+}
+
+#[test]
+fn invite_member_fails_with_bad_origin() {
+    build_test_externalities().execute_with(|| {
+        InviteMembershipFixture::default()
+            .with_origin(RawOrigin::None)
+            .call_and_assert(Err(Error::<Test>::UnsignedOrigin.into()));
+    });
+}
+
+#[test]
+fn invite_member_fails_with_invalid_member_id() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = DefaultMembershipPrice::get();
+        set_alice_free_balance(initial_balance);
+
+        assert_ok!(buy_default_membership_as_alice());
+        let invalid_member_id = 222;
+
+        InviteMembershipFixture::default()
+            .with_member_id(invalid_member_id)
+            .call_and_assert(Err(Error::<Test>::MemberProfileNotFound.into()));
+    });
+}
+
+#[test]
+fn invite_member_fails_with_not_controller_account() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = DefaultMembershipPrice::get();
+        set_alice_free_balance(initial_balance);
+
+        assert_ok!(buy_default_membership_as_alice());
+        let invalid_account_id = 222;
+
+        InviteMembershipFixture::default()
+            .with_accounts(invalid_account_id)
+            .with_origin(RawOrigin::Signed(invalid_account_id))
+            .call_and_assert(Err(Error::<Test>::ControllerAccountRequired.into()));
+    });
+}
+
+#[test]
+fn invite_member_fails_with_not_enough_invites() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        InviteMembershipFixture::default()
+            .call_and_assert(Err(Error::<Test>::NotEnoughInvites.into()));
+    });
+}
+
+#[test]
+fn invite_member_fails_with_bad_user_information() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = DefaultMembershipPrice::get();
+        set_alice_free_balance(initial_balance);
+
+        assert_ok!(buy_default_membership_as_alice());
+
+        InviteMembershipFixture::default()
+            .with_name([1u8; 5000].to_vec())
+            .call_and_assert(Err(Error::<Test>::NameTooLong.into()));
+
+        InviteMembershipFixture::default()
+            .with_avatar([1u8; 5000].to_vec())
+            .call_and_assert(Err(Error::<Test>::AvatarUriTooLong.into()));
+
+        InviteMembershipFixture::default()
+            .with_handle([1u8; 5000].to_vec())
+            .call_and_assert(Err(Error::<Test>::HandleTooLong.into()));
+
+        InviteMembershipFixture::default()
+            .with_handle(Vec::new())
+            .call_and_assert(Err(Error::<Test>::HandleTooShort.into()));
+
+        InviteMembershipFixture::default()
+            .with_empty_handle()
+            .call_and_assert(Err(
+                Error::<Test>::HandleMustBeProvidedDuringRegistration.into()
+            ));
+
+        let handle = b"Non-unique handle".to_vec();
+        InviteMembershipFixture::default()
+            .with_handle(handle.clone())
+            .call_and_assert(Ok(()));
+
+        InviteMembershipFixture::default()
+            .with_handle(handle)
+            .call_and_assert(Err(Error::<Test>::HandleAlreadyRegistered.into()));
+    });
+}
+
+#[test]
+fn set_membership_price_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        SetMembershipPriceFixture::default().call_and_assert(Ok(()));
+
+        EventFixture::assert_last_crate_event(Event::<Test>::MembershipPriceUpdated(
+            DEFAULT_MEMBERSHIP_PRICE,
+        ));
+    });
+}
+
+#[test]
+fn set_membership_price_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        SetMembershipPriceFixture::default()
+            .with_origin(RawOrigin::Signed(ALICE_ACCOUNT_ID))
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn buy_membership_with_zero_membership_price_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = 10000;
+        increase_total_balance_issuance_using_account_id(ALICE_ACCOUNT_ID, initial_balance);
+
+        // Set zero membership price
+        SetMembershipPriceFixture::default()
+            .with_price(0)
+            .call_and_assert(Ok(()));
+
+        // Try to buy membership.
+        BuyMembershipFixture::default().call_and_assert(Ok(()));
+
+        assert_eq!(Balances::usable_balance(&ALICE_ACCOUNT_ID), initial_balance);
+    });
+}
+
+#[test]
+fn set_leader_invitation_quota_succeeds() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        SetLeaderInvitationQuotaFixture::default().call_and_assert(Ok(()));
+
+        EventFixture::assert_last_crate_event(Event::<Test>::LeaderInvitationQuotaUpdated(
+            DEFAULT_LEADER_INVITATION_QUOTA,
+        ));
+    });
+}
+
+#[test]
+fn set_leader_invitation_quota_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        SetLeaderInvitationQuotaFixture::default()
+            .with_origin(RawOrigin::Signed(ALICE_ACCOUNT_ID))
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn set_leader_invitation_quota_fails_with_not_found_leader_membership() {
+    build_test_externalities().execute_with(|| {
+        SetLeaderInvitationQuotaFixture::default()
+            .call_and_assert(Err(Error::<Test>::MemberProfileNotFound.into()));
+    });
+}
+
+#[test]
+fn set_initial_invitation_balance_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        SetInitialInvitationBalanceFixture::default().call_and_assert(Ok(()));
+
+        EventFixture::assert_last_crate_event(Event::<Test>::InitialInvitationBalanceUpdated(
+            DEFAULT_INITIAL_INVITATION_BALANCE,
+        ));
+    });
+}
+
+#[test]
+fn set_initial_invitation_balance_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        SetInitialInvitationBalanceFixture::default()
+            .with_origin(RawOrigin::Signed(ALICE_ACCOUNT_ID))
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn set_initial_invitation_count_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        SetInitialInvitationCountFixture::default().call_and_assert(Ok(()));
+
+        EventFixture::assert_last_crate_event(Event::<Test>::InitialInvitationCountUpdated(
+            DEFAULT_INITIAL_INVITATION_COUNT,
+        ));
+    });
+}
+
+#[test]
+fn set_initial_invitation_count_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        SetInitialInvitationCountFixture::default()
+            .with_origin(RawOrigin::Signed(ALICE_ACCOUNT_ID))
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}

+ 8 - 2
runtime-modules/proposals/codex/src/tests/mock.rs

@@ -50,8 +50,9 @@ impl common::Trait for Test {
 
 impl membership::Trait for Test {
     type Event = ();
-    type MembershipFee = MembershipFee;
+    type DefaultMembershipPrice = DefaultMembershipPrice;
     type WorkingGroup = ();
+    type DefaultInitialInvitationBalance = ();
 }
 
 impl common::working_group::WorkingGroupIntegration<Test> for () {
@@ -61,11 +62,16 @@ impl common::working_group::WorkingGroupIntegration<Test> for () {
     ) -> DispatchResult {
         unimplemented!();
     }
+
+    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+        unimplemented!();
+    }
 }
 
 parameter_types! {
-    pub const MembershipFee: u64 = 100;
+    pub const DefaultMembershipPrice: u64 = 100;
     pub const ExistentialDeposit: u32 = 0;
+    pub const DefaultInitialInvitationBalance: u64 = 100;
 }
 
 impl balances::Trait for Test {

+ 1 - 1
runtime-modules/proposals/discussion/src/benchmarking.rs

@@ -150,7 +150,7 @@ benchmarks! {
         assert!(PostThreadIdByPostId::<T>::contains_key(thread_id, post_id), "Post not created");
 
         let new_text = vec![0u8; j.try_into().unwrap()];
-    }: _ (RawOrigin::Signed(account_id), caller_member_id, thread_id, post_id, new_text)
+    }: _ (RawOrigin::Signed(account_id), thread_id, post_id, new_text)
     verify {
         assert_last_event::<T>(RawEvent::PostUpdated(post_id, caller_member_id).into());
     }

+ 8 - 11
runtime-modules/proposals/discussion/src/lib.rs

@@ -133,9 +133,6 @@ pub trait Trait: frame_system::Trait + common::Trait {
 decl_error! {
     /// Discussion module predefined errors
     pub enum Error for Module<T: Trait> {
-        /// Author should match the post creator
-        NotAuthor,
-
         /// Thread doesn't exist
         ThreadDoesntExist,
 
@@ -240,23 +237,23 @@ decl_module! {
         #[weight = WeightInfoDiscussion::<T>::update_post()]
         pub fn update_post(
             origin,
-            post_author_id: MemberId<T>,
             thread_id: T::ThreadId,
             post_id : T::PostId,
             _text : Vec<u8>
         ){
+            ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
+            ensure!(
+                <PostThreadIdByPostId<T>>::contains_key(thread_id, post_id),
+                Error::<T>::PostDoesntExist
+            );
+
+            let post_author_id = <PostThreadIdByPostId<T>>::get(&thread_id, &post_id).author_id;
+
             T::AuthorOriginValidator::ensure_actor_origin(
                 origin,
                 post_author_id,
             )?;
 
-            ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
-            ensure!(<PostThreadIdByPostId<T>>::contains_key(thread_id, post_id), Error::<T>::PostDoesntExist);
-
-            let post = <PostThreadIdByPostId<T>>::get(&thread_id, &post_id);
-
-            ensure!(post.author_id == post_author_id, Error::<T>::NotAuthor);
-
             // mutation
 
             Self::deposit_event(RawEvent::PostUpdated(post_id, post_author_id));

+ 8 - 2
runtime-modules/proposals/discussion/src/tests/mock.rs

@@ -56,7 +56,8 @@ parameter_types! {
     pub const TransferFee: u32 = 0;
     pub const CreationFee: u32 = 0;
     pub const MaxWhiteListSize: u32 = 4;
-    pub const MembershipFee: u64 = 100;
+    pub const DefaultMembershipPrice: u64 = 100;
+    pub const DefaultInitialInvitationBalance: u64 = 100;
 }
 
 impl balances::Trait for Test {
@@ -80,8 +81,9 @@ impl common::Trait for Test {
 
 impl membership::Trait for Test {
     type Event = TestEvent;
-    type MembershipFee = MembershipFee;
+    type DefaultMembershipPrice = DefaultMembershipPrice;
     type WorkingGroup = ();
+    type DefaultInitialInvitationBalance = ();
 }
 
 impl common::working_group::WorkingGroupIntegration<Test> for () {
@@ -91,6 +93,10 @@ impl common::working_group::WorkingGroupIntegration<Test> for () {
     ) -> DispatchResult {
         unimplemented!();
     }
+
+    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+        unimplemented!();
+    }
 }
 
 impl crate::Trait for Test {

+ 4 - 7
runtime-modules/proposals/discussion/src/tests/mod.rs

@@ -146,7 +146,6 @@ impl PostFixture {
     fn update_post_with_text_and_assert(&mut self, new_text: Vec<u8>, result: DispatchResult) {
         let add_post_result = Discussions::update_post(
             self.origin.clone().into(),
-            self.author_id,
             self.thread_id,
             self.post_id.unwrap(),
             new_text,
@@ -263,17 +262,15 @@ fn update_post_call_fails_because_of_the_wrong_author() {
             .create_discussion_and_assert(Ok(1))
             .unwrap();
 
-        let mut post_fixture = PostFixture::default_for_thread(thread_id);
+        let mut post_fixture = PostFixture::default_for_thread(thread_id)
+            .with_origin(RawOrigin::Signed(12))
+            .with_author(12);
 
         post_fixture.add_post_and_assert(Ok(()));
 
-        post_fixture = post_fixture.with_author(5);
+        post_fixture = post_fixture.with_origin(RawOrigin::Signed(5));
 
         post_fixture.update_post_and_assert(Err(DispatchError::Other("Invalid author")));
-
-        post_fixture = post_fixture.with_origin(RawOrigin::None).with_author(2);
-
-        post_fixture.update_post_and_assert(Err(Error::<Test>::NotAuthor.into()));
     });
 }
 

+ 8 - 2
runtime-modules/proposals/engine/src/tests/mock/mod.rs

@@ -155,7 +155,8 @@ parameter_types! {
     pub const DescriptionMaxLength: u32 = 10000;
     pub const MaxActiveProposalLimit: u32 = 100;
     pub const LockId: LockIdentifier = [1; 8];
-    pub const MembershipFee: u64 = 100;
+    pub const DefaultMembershipPrice: u64 = 100;
+    pub const DefaultInitialInvitationBalance: u64 = 100;
 }
 
 impl common::Trait for Test {
@@ -165,8 +166,9 @@ impl common::Trait for Test {
 
 impl membership::Trait for Test {
     type Event = TestEvent;
-    type MembershipFee = MembershipFee;
+    type DefaultMembershipPrice = DefaultMembershipPrice;
     type WorkingGroup = ();
+    type DefaultInitialInvitationBalance = ();
 }
 
 impl common::working_group::WorkingGroupIntegration<Test> for () {
@@ -176,6 +178,10 @@ impl common::working_group::WorkingGroupIntegration<Test> for () {
     ) -> DispatchResult {
         unimplemented!();
     }
+
+    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+        unimplemented!();
+    }
 }
 
 impl crate::Trait for Test {

+ 8 - 2
runtime-modules/service-discovery/src/mock.rs

@@ -51,7 +51,8 @@ parameter_types! {
     pub const AvailableBlockRatio: Perbill = Perbill::one();
     pub const MinimumPeriod: u64 = 5;
     pub const ExistentialDeposit: u32 = 0;
-    pub const MembershipFee: u64 = 100;
+    pub const DefaultMembershipPrice: u64 = 100;
+    pub const DefaultInitialInvitationBalance: u64 = 100;
 }
 
 impl frame_system::Trait for Test {
@@ -98,8 +99,9 @@ impl common::Trait for Test {
 
 impl membership::Trait for Test {
     type Event = MetaEvent;
-    type MembershipFee = MembershipFee;
+    type DefaultMembershipPrice = DefaultMembershipPrice;
     type WorkingGroup = ();
+    type DefaultInitialInvitationBalance = ();
 }
 
 impl common::working_group::WorkingGroupIntegration<Test> for () {
@@ -109,6 +111,10 @@ impl common::working_group::WorkingGroupIntegration<Test> for () {
     ) -> DispatchResult {
         unimplemented!();
     }
+
+    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+        unimplemented!();
+    }
 }
 
 impl common::currency::GovernanceCurrency for Test {

+ 8 - 2
runtime-modules/staking-handler/src/mock.rs

@@ -24,7 +24,8 @@ parameter_types! {
     pub const AvailableBlockRatio: Perbill = Perbill::one();
     pub const MinimumPeriod: u64 = 5;
     pub const ExistentialDeposit: u32 = 0;
-    pub const MembershipFee: u64 = 100;
+    pub const DefaultMembershipPrice: u64 = 100;
+    pub const DefaultInitialInvitationBalance: u64 = 100;
 }
 
 // Workaround for https://github.com/rust-lang/rust/issues/26925 - remove when sorted.
@@ -76,8 +77,9 @@ impl common::Trait for Test {
 
 impl membership::Trait for Test {
     type Event = ();
-    type MembershipFee = MembershipFee;
+    type DefaultMembershipPrice = DefaultMembershipPrice;
     type WorkingGroup = ();
+    type DefaultInitialInvitationBalance = ();
 }
 
 impl LockComparator<<Test as pallet_balances::Trait>::Balance> for Test {
@@ -101,6 +103,10 @@ impl common::working_group::WorkingGroupIntegration<Test> for () {
     ) -> DispatchResult {
         unimplemented!();
     }
+
+    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+        unimplemented!();
+    }
 }
 
 impl pallet_timestamp::Trait for Test {

+ 8 - 2
runtime-modules/storage/src/tests/mock.rs

@@ -156,7 +156,8 @@ impl GovernanceCurrency for Test {
 parameter_types! {
     pub const MaxWorkerNumberLimit: u32 = 3;
     pub const LockId: LockIdentifier = [2; 8];
-    pub const MembershipFee: u64 = 100;
+    pub const DefaultMembershipPrice: u64 = 100;
+    pub const DefaultInitialInvitationBalance: u64 = 100;
 }
 
 pub struct WorkingGroupWeightInfo;
@@ -283,8 +284,9 @@ impl common::Trait for Test {
 
 impl membership::Trait for Test {
     type Event = MetaEvent;
-    type MembershipFee = MembershipFee;
+    type DefaultMembershipPrice = DefaultMembershipPrice;
     type WorkingGroup = ();
+    type DefaultInitialInvitationBalance = ();
 }
 
 impl common::working_group::WorkingGroupIntegration<Test> for () {
@@ -294,6 +296,10 @@ impl common::working_group::WorkingGroupIntegration<Test> for () {
     ) -> DispatchResult {
         unimplemented!();
     }
+
+    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+        unimplemented!();
+    }
 }
 
 impl minting::Trait for Test {

+ 7 - 0
runtime-modules/working-group/src/lib.rs

@@ -1337,4 +1337,11 @@ impl<T: Trait<I>, I: Instance> common::working_group::WorkingGroupIntegration<T>
     fn ensure_worker_origin(origin: T::Origin, worker_id: &WorkerId<T>) -> DispatchResult {
         checks::ensure_worker_signed::<T, I>(origin, worker_id).map(|_| ())
     }
+
+    fn get_leader_member_id() -> Option<T::MemberId> {
+        checks::ensure_lead_is_set::<T, I>()
+            .map(Self::worker_by_id)
+            .map(|worker| worker.member_id)
+            .ok()
+    }
 }

+ 4 - 2
runtime-modules/working-group/src/tests/mock.rs

@@ -37,7 +37,8 @@ parameter_types! {
     pub const AvailableBlockRatio: Perbill = Perbill::one();
     pub const MinimumPeriod: u64 = 5;
     pub const ExistentialDeposit: u32 = 0;
-    pub const MembershipFee: u64 = 0;
+    pub const DefaultMembershipPrice: u64 = 0;
+    pub const DefaultInitialInvitationBalance: u64 = 100;
 }
 
 // Workaround for https://github.com/rust-lang/rust/issues/26925 - remove when sorted.
@@ -100,8 +101,9 @@ impl common::Trait for Test {
 
 impl membership::Trait for Test {
     type Event = TestEvent;
-    type MembershipFee = MembershipFee;
+    type DefaultMembershipPrice = DefaultMembershipPrice;
     type WorkingGroup = Module<Test>;
+    type DefaultInitialInvitationBalance = ();
 }
 
 impl LockComparator<<Test as balances::Trait>::Balance> for Test {

+ 9 - 4
runtime/src/lib.rs

@@ -572,8 +572,11 @@ impl council::Trait for Runtime {
         membership_id: &Self::MemberId,
         account_id: &<Self as frame_system::Trait>::AccountId,
     ) -> bool {
-        membership::Module::<Runtime>::ensure_member_controller_account(account_id, membership_id)
-            .is_ok()
+        membership::Module::<Runtime>::ensure_is_controller_account_for_member(
+            membership_id,
+            account_id,
+        )
+        .is_ok()
     }
 
     fn new_council_elected(_elected_members: &[council::CouncilMemberOf<Self>]) {
@@ -588,7 +591,7 @@ impl memo::Trait for Runtime {
 
 parameter_types! {
     pub const MaxObjectsPerInjection: u32 = 100;
-    pub const MembershipFee: Balance = 100;
+    pub const DefaultMembershipPrice: Balance = 100;
 }
 
 impl storage::data_object_type_registry::Trait for Runtime {
@@ -618,11 +621,13 @@ impl common::Trait for Runtime {
 
 impl membership::Trait for Runtime {
     type Event = Event;
-    type MembershipFee = MembershipFee;
+    type DefaultMembershipPrice = DefaultMembershipPrice;
     type WorkingGroup = MembershipWorkingGroup;
+    type DefaultInitialInvitationBalance = DefaultInitialInvitationBalance;
 }
 
 parameter_types! {
+    pub const DefaultInitialInvitationBalance: Balance = 100;
     pub const MaxCategoryDepth: u64 = 5;
     pub const MaxSubcategories: u64 = 20;
     pub const MaxThreadsInCategory: u64 = 20;

+ 1 - 1
runtime/src/tests/mod.rs

@@ -145,7 +145,7 @@ pub(crate) fn elect_council(council: Vec<AccountId32>, cycle_id: u64) {
 pub(crate) fn insert_member(account_id: AccountId32) {
     increase_total_balance_issuance_using_account_id(
         account_id.clone(),
-        crate::MembershipFee::get(),
+        crate::DefaultMembershipPrice::get(),
     );
     let handle: &[u8] = account_id.as_ref();