├── .gitignore ├── README.md ├── app.py ├── database ├── connection_string.py ├── csv_migration.py ├── database_queries.py └── dummy-data.csv ├── models ├── __init__.py └── user_models.py ├── requirements.txt ├── routers ├── __init__.py ├── checkReferralCode.py ├── getCode.py ├── redeemCode.py └── registerUser.py ├── services ├── __init__.py └── referral_system.py └── tests ├── __init__.py └── test_functions.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | test.db 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-referral-system 2 | 3 | A referral system built using FastAPI and SQLite3. This system allows users to generate referral codes, check status of referral codes, redeem referral codes and invalidate codes after a set stale period. 4 | 5 | # Features 6 | 7 | - [x] Built-in Referral Code Expiry 8 | - [x] Generate referral code 9 | - [x] Check validity of referral Code 10 | - [x] Redeem referral code 11 | - [x] Strong types and type enforcing 12 | 13 | # Core Requirements 14 | 15 | - FastAPI 16 | - SQLite3 17 | 18 | # Project Setup 19 | 20 | 1. Make sure you have SQLite3 installed on your machine. Next, Clone this repo and navigate to the root directory. 21 | 22 | ```bash 23 | git clone https://github.com/buabaj/python-referral-system.git 24 | cd python-referral-system 25 | ``` 26 | 27 | 2. Create and activate a virtual environment for the requirements of your project. 28 | 29 | ```bash 30 | python3 -m venv .venv/ 31 | source .venv/bin/activate 32 | pip install -r requirements.txt 33 | ``` 34 | 35 | 3. Start your sqlite3 server in your terminal using the command: 36 | 37 | ```bash 38 | sqlite3 test.db 39 | ``` 40 | 41 | 4. Paste the following SQL command in the sqlite3 prompt that comes on in your terminal to create your database tables: 42 | 43 | ```SQL 44 | CREATE TABLE Users( 45 | user_id INTEGER PRIMARY KEY AutoIncrement, 46 | first_name TEXT NOT NULL, 47 | last_name TEXT NOT NULL, 48 | phone TEXT, 49 | email TEXT NOT NULL, 50 | token TEXT, 51 | created_at DATE, 52 | updated_at DATE 53 | ); 54 | 55 | CREATE TABLE Referrals( 56 | referral_code_id INTEGER PRIMARY KEY AutoIncrement, 57 | referral_code TEXT NOT NULL, 58 | created_at DATE, 59 | is_active boolean NOT NULL, 60 | user_id INT NOT NULL, 61 | foreign key(user_id) references Users(user_id) 62 | 63 | ); 64 | ``` 65 | 66 | 5. Run the following code in a different terminal to run your local FastAPI server: 67 | 68 | ```bash 69 | python3 app.py 70 | ``` 71 | 72 | 6. Head to `localhost:8000/docs` in your browser to test the Implementation of the referral system endpoints and documentation in an interactive SWAGGER UI API Documentation playground. 73 | 74 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI 3 | from routers.registerUser import router as registerUserRouter 4 | from routers.redeemCode import router as redeemCodeRouter 5 | from routers.getCode import router as getCodeRouter 6 | from routers.checkReferralCode import router as checkReferralCodeRouter 7 | 8 | app = FastAPI() 9 | 10 | app.include_router(registerUserRouter) 11 | app.include_router(redeemCodeRouter) 12 | app.include_router(getCodeRouter) 13 | app.include_router(checkReferralCodeRouter) 14 | 15 | 16 | # create hello world endpoint 17 | @app.get('/') 18 | async def root(): 19 | return {'message': 'Hello world'} 20 | 21 | 22 | if __name__ == "__main__": 23 | uvicorn.run(app) 24 | -------------------------------------------------------------------------------- /database/connection_string.py: -------------------------------------------------------------------------------- 1 | """insert database connection string here check pydapper for connection string template""" 2 | connection_string = "sqlite://test.db" -------------------------------------------------------------------------------- /database/csv_migration.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pydapper import connect 3 | from database.database_queries import fetch_user_by_email 4 | from services.referral_system import referral_code_handler 5 | from database.connection_string import connection_string 6 | 7 | """inserts list of users into database and calls referral_code_handler to generate referral codes""" 8 | 9 | 10 | def insert_users_from_csv(csv_filename): 11 | with open(csv_filename, 'r') as csvfile: 12 | users = csv.DictReader(csvfile) 13 | 14 | for user in users: 15 | with connect(connection_string) as commands: 16 | commands.execute( 17 | "insert into users(first_name, last_name,phone, email,token, created_at, updated_at ) values(" 18 | "?first_name?, ?last_name?, ?phone?, ?email?, ?token?, ?created_at?, ?updated_at?)", 19 | param={"first_name": user["first_name"], "last_name": user["last_name"], "phone": user["phone"], 20 | "email": user["email"], "token": user["token"], "created_at": user["created_at"], 21 | "updated_at": user["updated_at "]}) 22 | 23 | referral_code_handler(fetch_user_by_email(user["email"])) 24 | 25 | 26 | insert_users_from_csv("dummy-data.csv") 27 | -------------------------------------------------------------------------------- /database/database_queries.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pydapper import connect 4 | from database.connection_string import connection_string 5 | from models.user_models import User 6 | 7 | 8 | def fetch_user_by_email(email: str) -> int: 9 | with connect(connection_string) as commands: 10 | user_id_dict = commands.query_single( 11 | "select user_id from users where email = ?email?", param={"email": email}) 12 | return user_id_dict["user_id"] 13 | 14 | 15 | def insert_user(registration_details: User): 16 | with connect(connection_string) as commands: 17 | commands.execute("insert into users(first_name, last_name, phone, email, token, created_at, updated_at) " 18 | "values(?first_name?, ?last_name?, ?phone?, ?email?, ?token?, ?created_at?, ?updated_at?)", 19 | param={"first_name": registration_details.first_name, "last_name": 20 | registration_details.last_name, "phone": registration_details.phone, 21 | "email": registration_details.email, "token": registration_details.token, 22 | "created_at": registration_details.created_at, "updated_at": 23 | registration_details.updated_at}) 24 | 25 | 26 | def user_exists(email: str) -> bool: 27 | with connect(connection_string) as commands: 28 | token = commands.query( 29 | "select token from users where email = ?email?", param={"email": email}) 30 | 31 | print(token) 32 | if token == []: 33 | return False 34 | return True 35 | 36 | 37 | def fetch_referral_code(email: str) -> str: 38 | if user_exists(email): 39 | with connect(connection_string) as commands: 40 | referral_code = commands.query_single( 41 | "select referral_code from Users inner join Referrals on Users.user_id= referrals.user_id where email " 42 | "= ?email?", 43 | param={"email": email}) 44 | return referral_code["referral_code"] 45 | 46 | 47 | def store_referral_code(referral_code, user_id: int): 48 | with connect(connection_string) as commands: 49 | commands.execute( 50 | "insert into Referrals ( referral_code, created_at, user_id, is_active) values(?referral_code?, " 51 | "?created_at?, ?user_id?, ?is_active?)", 52 | param={"referral_code": referral_code, "created_at": datetime.datetime.now(), "user_id": user_id, 53 | "is_active": True}) 54 | 55 | 56 | def inactivate_referral_token(code): 57 | with connect(connection_string) as commands: 58 | commands.execute( 59 | "update Referrals set is_active = ?is_active? where referral_code = ?referral_code?", 60 | param={"is_active": False, "referral_code": code}) 61 | 62 | 63 | def delete_referral_token(code): 64 | with connect(connection_string) as commands: 65 | commands.execute( 66 | "delete from Referrals where referral_code = ?referral_code?", 67 | param={"referral_code": code}) 68 | -------------------------------------------------------------------------------- /database/dummy-data.csv: -------------------------------------------------------------------------------- 1 | user_id,first_name,last_name,phone,email,token,created_at,updated_at 2 | 1,Jerry,Rigman,123456789,to@hustle.app,8923bcanlnebr9283hdlamnajv927293,12/3/2021,12/3/2021 3 | 2,Osman,Laden,123456789,ol@yahoo.com,pj09dj0293dnlkcnbv2739honlkcwj3902,12/16/2021,12/16/2021 4 | 3,Uncle,Obama,123456789,ub@letme.as,h308hnocljkqbf3892ojfpkjner829hnfeifj,12/18/2021,12/18/2021 5 | 4,Susan,Lorem,123456789,susan@hello.app,9038jeuncby7uehbuy628u3ew89i2eu9,12/22/2021,12/22/2021 6 | 5,Lorem,Ipsum,123456789,getem@sales.com,901ndjewby831p908duwhby82wkjnciu,12/11/2021,12/11/2021 7 | 6,Jeffry,Rice,123456789,ell@huop.app,921uwehb69gbcetw7631gbdn281nes8,12/8/2021,12/8/2021 8 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buabaj/python-referral-system/48514453e7ccd776532b2e05bc93fdb8f75950d5/models/__init__.py -------------------------------------------------------------------------------- /models/user_models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pydantic import BaseModel, EmailStr 3 | 4 | 5 | class User(BaseModel): 6 | first_name: str 7 | last_name: str 8 | phone: str 9 | email: EmailStr 10 | token: str 11 | created_at: datetime 12 | updated_at: datetime 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.5.0 2 | asgiref==3.5.0 3 | attrs==21.4.0 4 | autopep8==1.6.0 5 | cached-property==1.5.2 6 | certifi==2021.10.8 7 | charset-normalizer==2.0.11 8 | click==8.0.3 9 | dnspython==2.2.1 10 | dsnparse==0.1.15 11 | email-validator==1.1.3 12 | fastapi==0.73.0 13 | greenlet==1.1.2 14 | gunicorn==20.1.0 15 | h11==0.13.0 16 | idna==3.3 17 | iniconfig==1.1.1 18 | numpy==1.22.3 19 | packaging==21.3 20 | pandas==1.4.1 21 | pluggy==1.0.0 22 | psycopg2-binary==2.9.3 23 | py==1.11.0 24 | pycodestyle==2.8.0 25 | pydantic==1.9.0 26 | pydapper==0.4.0 27 | pyparsing==3.0.7 28 | pytest==6.2.5 29 | python-dateutil==2.8.2 30 | python-dotenv==0.19.2 31 | python-multipart==0.0.5 32 | pytz==2022.1 33 | requests==2.27.1 34 | six==1.16.0 35 | sniffio==1.2.0 36 | SQLAlchemy==1.4.31 37 | starlette==0.17.1 38 | toml==0.10.2 39 | typing-extensions==4.0.1 40 | urllib3==1.26.8 41 | uvicorn==0.17.3 42 | -------------------------------------------------------------------------------- /routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buabaj/python-referral-system/48514453e7ccd776532b2e05bc93fdb8f75950d5/routers/__init__.py -------------------------------------------------------------------------------- /routers/checkReferralCode.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | from services.referral_system import is_active_referral_code 3 | 4 | 5 | router = APIRouter() 6 | 7 | 8 | @router.post("/check_referral_code_validity") 9 | async def check_referral_code_validity(referral_code: str): 10 | try: 11 | return is_active_referral_code(referral_code) 12 | except Exception: 13 | raise HTTPException( 14 | status_code=400, detail="Referral code is not valid") 15 | -------------------------------------------------------------------------------- /routers/getCode.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from database.database_queries import fetch_referral_code 3 | 4 | 5 | router = APIRouter() 6 | 7 | 8 | @router.post("/get_referral_code") 9 | async def get_referral_code(email: str): 10 | return fetch_referral_code(email) 11 | -------------------------------------------------------------------------------- /routers/redeemCode.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from services.referral_system import redeem_referral 3 | 4 | 5 | router = APIRouter() 6 | 7 | 8 | @router.post("/redeem_referral_code") 9 | def redeem_referral_code(referral_code: str): 10 | redeem_referral(referral_code) 11 | return {"message": "Referral code redeemed successfully"} 12 | -------------------------------------------------------------------------------- /routers/registerUser.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | from models.user_models import User 3 | from services.referral_system import referral_code_handler 4 | from database.database_queries import user_exists, insert_user, fetch_user_by_email 5 | 6 | 7 | router = APIRouter() 8 | 9 | 10 | @router.post("/register_user") 11 | async def register_user(user_details: User): 12 | if user_exists(user_details.email): 13 | raise HTTPException(status_code=400, detail="User already exists") 14 | else: 15 | insert_user(user_details) 16 | referral_code_handler(fetch_user_by_email(user_details.email)) 17 | return {"message": "User registered successfully"} 18 | -------------------------------------------------------------------------------- /services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buabaj/python-referral-system/48514453e7ccd776532b2e05bc93fdb8f75950d5/services/__init__.py -------------------------------------------------------------------------------- /services/referral_system.py: -------------------------------------------------------------------------------- 1 | from database.connection_string import connection_string 2 | from pydapper import connect 3 | import secrets 4 | import threading 5 | 6 | from database.database_queries import store_referral_code, inactivate_referral_token, delete_referral_token 7 | 8 | stale_time = 60 * 60 * 24 * 2 9 | 10 | 11 | def referral_code_handler(user_id: int): 12 | store_referral_code(generate_referral_code(), user_id) 13 | 14 | 15 | def generate_referral_code() -> str: 16 | code = secrets.token_urlsafe(8) 17 | expire_referral_code(code) 18 | return code 19 | 20 | 21 | def is_active_referral_code(code: str) -> bool: 22 | is_active_dict: int 23 | with connect(connection_string) as commands: 24 | is_active_dict = commands.query_single( 25 | "select is_active from Referrals where referral_code = ?referral_code?", 26 | param={"referral_code": code}) 27 | 28 | if is_active_dict["is_active"]: 29 | return True 30 | else: 31 | return False 32 | 33 | 34 | def redeem_referral(code: str): 35 | invalidate_referral_code(code) 36 | 37 | 38 | def invalidate_referral_code(code: str): 39 | inactivate_referral_token(code) 40 | 41 | 42 | def set_interval_for_code_invalidation(func, sec): 43 | def func_wrapper(): 44 | set_interval_for_code_invalidation(func, sec) 45 | func() 46 | 47 | t = threading.Timer(sec, func_wrapper) 48 | t.start() 49 | return t 50 | 51 | 52 | def expire_referral_code(code: str): 53 | set_interval_for_code_invalidation(invalidate_referral_code, stale_time) 54 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buabaj/python-referral-system/48514453e7ccd776532b2e05bc93fdb8f75950d5/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_functions.py: -------------------------------------------------------------------------------- 1 | from http import client 2 | from fastapi.testclient import TestClient 3 | from app import app 4 | 5 | 6 | client = TestClient(app) 7 | 8 | # test index route for 200 status code 9 | 10 | 11 | def test_index_route(): 12 | response = client.get("/") 13 | assert response.status_code == 200 14 | 15 | # test register user route for 200 status code 16 | 17 | 18 | def test_register_user_route(): 19 | response = client.post("/register_user", json={ 20 | "first_name": "jerry", 21 | "last_name": "buaba", 22 | "phone": "504782862", 23 | "email": "jerrytest@example.com", 24 | "token": "1234567890", 25 | "created_at": "2022-03-25T08:27:07.703Z", 26 | "updated_at": "2022-03-25T08:27:07.703Z" 27 | }) 28 | assert response.status_code == 200 29 | 30 | # test get referral code route for 200 status code 31 | 32 | 33 | def test_get_referral_code_route(): 34 | response = client.post("/get_referral_code", json={ 35 | "email": "jerrytest@example.com"}) 36 | assert response.status_code == 200 37 | code = response.json() 38 | return code 39 | 40 | # test check referral code validity route for 200 status code 41 | 42 | 43 | def test_check_referral_code_validity_route(code): 44 | response = client.post("/check_referral_code_validity", json={ 45 | "referral_code": test_get_referral_code_route()}) 46 | assert response.status_code == 200 47 | 48 | # test redeem referral code route for 200 status code 49 | 50 | 51 | def test_redeem_referral_code_route(code): 52 | response = client.post("/redeem_referral_code", json={ 53 | "referral_code": test_get_referral_code_route()}) 54 | assert response.status_code == 200 55 | --------------------------------------------------------------------------------