Clone (HTTPS): https://github.com/SteaneMurphy/JamesMurphy_T1A2
This project is a working API Flask web-service using a PostgreSQL backend. It has been designed using MVC and RESTful API architecture with a 3NF (3rd Normal Form) database schema.
This API provides data to any front-end application or service in the form of JSON objects. The service is a mock-up SaaS system whereby users can register and login to the system, purchase products through subscriptions and the system invoices the customer for said purchase. Each product comes with attatched licensing.
This API helps an online business manage customers and the products and services they purchase as well as allowing the business to update or change a product/service at will. The product/service can be offered through a subscription by having the system automate billing and payments.
This solution reduces the amount of contact hours an employee of the business needs to spend organising customer data, preparing invoices and checking subscription and payment statuses by providing the data to automate the procedure.
Alongside productivity boosts, a SaaS product has a high chance of increasing business income due to its customer ease-of-use, ability to adapt to the market and offer the ability to subscribe to a service rather than paying an expensive upfront fee. This structure is seen in popular software like the Adobe suite, which in times past, used to cost thoudands of dollars per seat.
Other benefits include software that runs on the web or from the cloud, removing the need for high IT costs and managing licenses, freeing up staff to hot-desk or work remotely.
In 2024 (Vena Solutions, 2024, 74 SaaS Statistics, Trends and Benchmarks for 2024, accessed, 20 June 2024, https://www.venasolutions.com/blog/saas-statistics):
73% of business surveyed, said that they spend too much time manually doing tasks that may be able to be automated
60% of surveyed business claim to be budgeting to spend 'more on software' in the coming year
50% of those surveyed mentioned they will increase their use of cloud providers in the coming years
All completed and outstanding tasks for this project can be found on its associated Github Projects board.
The project is split into 7 parent tasks, comprised of smaller individual tasks:
It is possible that further tasks will be added as issues are encountered throughtout the project.
These tasks are tracked across four stages of completion:
This application uses the following dependencies:
Flask
JWT Extended
SQLAlchemy
SQLAlchemy ORM
Marshmallow
BCrypt
PostgreSQL
These are only major dependencies, it is recommended that a user installs the full list of dependencies from the "requirements.txt" file included in this repository.
The following are installation instructions to get his application working in your environment.
To clone the repository, please make a directory for the project. This can be done by making a new folder in Windows or by typing the following into a terminal:
mkdir <directory_path>
The Python Virtual Environment package will allow a user to modify their project environment without affecting the overall system. To install this package, please enter the following command into the terminal:
pip install venv
Once the virtual environment package has been installed, we need to create a new virtual environment. To create a new virtual environment, please use the following command:
python3 -m venv .venv
This new environment must be activated:
source .venv/bin/activate
Ensuring that you are within the newly created virtual environment, you can safely install the dependencies listed in the "requirements.txt". To iterate over the list and install each package, please use the following terminal command:
pip install -r requirements.txt
Included in the documents folder is a list of endpoints that can be imported into the Insomnia API software. This can help if you don't want to manually create HTTPS body requests and URIs.
You will need to create a new postgreSQL database and fill out the database details in the .env.sample
including databse username and password. The .env.sample
will need its file extenstion changed to .env
by removing the .sample
extenstion
To seed the database or run an instance of the the Flask service. Please use the following commands in terminal, when in the project folder.
Flask cli create
- recreates and seeds the database
Flask run
- runs the Flask local service
The local service runs on HTTPS://127.0.0.1/5000
You cannot create an admin account from accoutn creation and will need to use the existing admin account when the database is seeded. This admin account can make other accounts, admin by using the appropriate endpoint.
HTTPS://127.0.0.1/5000/users/login/
{
"email": "admin_email_1",
"password": "testpassword123"
}
PostgreSQL was the database used for this API. Whilst it is a powerful and open-source system, it also has its drawbacks including in its use as a relational database system.
PostgreSQL is open-source and therefore free. Due to this, the software is not limited by vision is quite extensible, allowing for the use of a multitude of third-party services, packages and libraries.
This database uses 'ACID' (Atomicity, Consistency, Isolation, Durability). In short, it ensures that each database transaction is treated as a single instance, meaning that in the case of an error, the database can fall-back to its previous state. Postgres ensures that each end of a transaction force the use of the original data type, increasing reliability.
The database has access to multiple built in security features allowing users to be authenicated down to specific tables within a system.
Configuring the database to work with thrid-party software and libraries can be time consuming and complicated. Proprietary database software tends to have all required features built-in.
Postgres is a relational database and thus uses MVCC (multi-concurrency) meaning that it stores multiple versions of data at any one time as part of its reliability and 'ACID' compliance. This can lead to much more storage space required for very large databases.
Dead fields and data are not automatically cleaned up out of a Postgres database and manual cleaning called 'vacuuming' must be undertaken periodically. This can slow down system resources considerably and does not allow database transactions to take place during this process. This may not be suitable for systems that can have no down-time.
SQLAlchemy is the ORM (Object-Relational Mapping) system used in this API. The ORM allowed access to read and manipulate the database using python objects rather than SQL language.
SQLAlchemy allows the creation of database schemas or tables directly within the API using python classes. This was extended with the uses of the 'Declarative' and 'Mapped' extenstions to SQLAlchemy. Columns and data types can be created directly inside a python class in a similar fashion to how the tables would look within the database. As follows is a code example utilising specific data types, primary and foreign keys, attribute creation and mapping.
SQlAlchemy also helps support relationships between database tables through the use of the 'relationship' and 'ForeignKey' function and types. These relationships can be back-referenced to parent tables and help facilitate cascade deletes. The ORM supports one-to-one, many-to-one and many-to-many relationship types.
SQLAlchemy also allows for database querying and retrieval of data as well as database session management. The database queries are abstracted from the base SQL query and easier to use. For all intents and purposes, the ORM keeps the SQL syntax similar with terms like 'select', 'join' and 'where'.
The database session can be managed with terms like 'session.commit' and 'session.flush'.
The following is an ERD diagram for the proposed database system used by this API. The ERD has been normalised to 3rd normal form (3NF). All tables have had duplication removed and have a unique id to their respective entity. Each table has its proposed data types, constraints and relationships notated. A unique id can appear on another table as a foreign key (FK) associating the two tables together.
I arrived at the the 3NF by normalising the data through the 1st normal form (1NF) and 2nd normal form (2NF) respectively.
Each attribute in the table (row and column) should be unique without duplication. Each attribute was reduced to a singular format, for example, each row in the 'product' table contains a singular 'product' not multiple 'products'. Each table was assigned a primary key, in the case of this ERD, they are all ID values.
For 2nd normal form, each attribute that was not a key was made 'independent', that is it is only dependent on the primary key in the table it appears, it does not rely on another table's primary key when being referenced. An example of this is the date values in the 'user' table. They can only be accessed by using the primary key in the 'users' table, they are independent of the other tables.
All duplication has been removed. Tables that rely on each other in a many-to-many relationship are abstracted by having their primary keys appear as combination-keys in an association table, for example, the subscription_id, product_id and license attributes in the 'subscription_details' table.
Before and during development of the API, multiple relationships were identified and developed between the entity models:
Subscription/Subscription_Details: a one-to-one/many relationship. This is one-half of a many-to-many relationship with the product table. As a product can appear in multiple subscriptions they are made unqiue by an association table. One FK(subscription_id) may be associated with many different FK(product_id). This relationship allow for a subscription to be associated with more than one product at a time and allows for database queries that can list all products associated to said subscription.
Product/Subscription_Details: a one-to-one/many relationship. As explained above, this is the other half of a many-to-many relationship between the subscription and product tables. Please see above for relationship and database query explanations.
/users/
POST
{
"first_name": "string",
"last_name": "string",
"email": "string",
"password": "string",
"admin": bool
}
{
"date_created": date.today(),
"email": "string",
"first_name": "string",
"id": int,
"last_login": date.today(),
"last_name": "string"
}
{
"error": "bad request: body missing information"
}
{
"error": "field missing information"
}
/users/login/
POST
{
"email": "string",
"password": "string",
}
{
//token used is an example JWT token
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcxOTY1NzAyOSwianRpIjoiZTYxMWY5ZjgtNWI4Zi00YTIxLThkODgtZTQyYTQ1ZjUxOWE3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MiwibmJmIjoxNzE5NjU3MDI5LCJjc3JmIjoiZjM0NmIxNjEtMWE0My00NjE0LTgzMjYtZWFmMzlhMzYxNGY3IiwiZXhwIjoxNzE5NjY0MjI5fQ.E2B2FkIUyfhToriSzG2I87YzRqq8A8spRXN9vLVGbG4"
}
{
"error": "field missing information"
}
{
"error": "email and/or password incorrect""
}
/users/<int:id>
GET
admin: JWT token
or acc.owner: JWT token
N/A
{
"date_created": date,
"email": "string",
"first_name": "string",
"id": int,
"last_login": date,
"last_name": "string"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/users/
GET
admin: JWT token
N/A
{
//entry 1
"admin": bool,
"date_created": date,
"first_name": "string",
"last_name": "string",
"email": "string",
"id": int,
"last_login": date
}
{
//entry 2... etc
}
{
"error": "not authorised to access this resource"
}
/users/admin/<int:id>
PUT
admin: JWT token
{
"admin": bool
}
{
"success": "account admin privileges updated"
}
{
"error": "bad request: body missing information"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/users/<int:id>
PUT
admin: JWT token
or acc.owner: JWT token
{
"first_name": "string",
"last_name": "string",
"email": "string",
}
{
"date_created": date.today(),
"email": "string",
"first_name": "string",
"id": int,
"last_login": date.today(),
"last_name": "string"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/users/credentials/<int:id>
PUT
admin: JWT token
or acc.owner: JWT token
{
"password": "string",
}
{
"success": "password updated"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/users/<int:id>
DELETE
admin: JWT token
N/A
{}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/subscriptions/
POST
admin: JWT token
or acc.owner: JWT token
{
"status": bool,
"plan_id" int,
"products": [int, int, ...]
}
{
"end_date": date,
"id": int,
"plan": {
"description": "string",
"id": int,
"length": int,
"plan_name": "string",
"price": float,
"product_limit": int
},
"plan_id": int,
"start_date": date,
"status": bool,
"user": {
"id": int
}
}
{
"error": "bad request: body missing information"
}
{
"error": "not authorised to access this resource"
}
/subscriptions/<int:id>
GET
admin: JWT token
or acc.owner: JWT token
N/A
{
"end_date": date,
"id": int,
"plan": {
"description": "string",
"id": int,
"length": int,
"plan_name": "string",
"price": float,
"product_limit": int
},
"plan_id": int,
"start_date": date,
"status": bool,
"user": {
"id": int
}
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/subscriptions/user/<int:id>
GET
admin: JWT token
or acc.owner: JWT token
N/A
{
//entry 1
"end_date": date,
"id": int,
"plan": {
"description": "string",
"id": int,
"length": int,
"plan_name": "string",
"price": float,
"product_limit": int
},
"plan_id": int,
"start_date": date,
"status": bool,
"user": {
"id": int
}
},
{
//entry 2... etc
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/subscriptions/
GET
admin: JWT token
N/A
{
//entry 1
"end_date": date,
"id": int,
"plan": {
"description": "string",
"id": int,
"length": int,
"plan_name": "string",
"price": float,
"product_limit": int
},
"plan_id": int,
"start_date": date,
"status": bool,
"user": {
"id": int
}
},
{
//entry 2... etc
}
{
"error": "not authorised to access this resource"
}
/subscriptions/<int:id>
PUT
admin: JWT token
{
"status": bool
}
{
"id": int,
"status": bool,
"user": {
"id": int
}
}
{
"error": "bad request: body missing information"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/subscriptions/<int:id>
DELETE
admin: JWT token
N/A
{}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/payments/<int:id>
POST
admin: JWT token
{
"payment_type": "string"
}
{
"amount": float,
"id": int,
"payment_date": date.today(),
"payment_type": "string",
"subscription": {
"id": int
}
}
{
"error": "bad request: body missing information"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/payments/<int:id>
GET
admin: JWT token
or acc.owner: JWT token
N/A
{
"amount": float,
"id": int,
"payment_date": date,
"payment_type": "string",
"subscription": {
"id": int
}
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/payments/user/<int:id>
GET
admin: JWT token
or acc.owner: JWT token
N/A
{
//entry 1
"amount": float,
"id": int,
"payment_date": date,
"payment_type": "string",
"subscription": {
"id": int
}
},
{
//entry 2...etc
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/payments/
GET
admin: JWT token
N/A
{
//entry 1
"amount": float,
"id": int,
"payment_date": date,
"payment_type": "string",
"subscription": {
"id": int
}
},
{
//entry 2...etc
}
{
"error": "not authorised to access this resource"
}
/payments/<int:id>
PUT
admin: JWT token
{
"amount": float,
"payment_type": "string"
}
{
//entry 1
"amount": float,
"id": int,
"payment_date": date,
"payment_type": "string",
"subscription": {
"id": int
}
}
{
"error": "bad request: body missing information"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/payments/<int:id>
DELETE
admin: JWT token
N/A
{}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/tickets/
POST
admin: JWT token
or acc.owner: JWT token
{
"issue_description": "string",
"status": "string"
}
{
"date_created": date,
"id": int,
"issue_description": "string",
"status": "string",
"user": {
"id": int
}
}
{
"error": "bad request: body missing information"
}
{
"error": "not authorised to access this resource"
}
/tickets/<int:id>
GET
admin: JWT token
or acc.owner: JWT token
N/A
{
"date_created": date,
"id": int,
"issue_description": "string",
"status": "string",
"user": {
"id": int
}
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/tickets/user/<int:id>
GET
admin: JWT token
or acc.owner: JWT token
N/A
{
//entry 1
"date_created": date,
"id": int,
"issue_description": "string",
"status": "string",
"user": {
"id": int
}
},
{
//entry 2...etc
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/tickets/
GET
admin: JWT token
N/A
{
//entry 1
"date_created": date,
"id": int,
"issue_description": "string",
"status": "string",
"user": {
"id": int
}
},
{
//entry 2...etc
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/tickets/<int:id>
PUT
admin: JWT token
or acc.owner: JWT token
{
"issue_description": "string",
"status": "string"
}
{
"date_created": date,
"id": int,
"issue_description": "string",
"status": "string",
"user": {
"id": int
}
}
{
"error": "bad request: body missing information"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/tickets/<int:id>
DELETE
admin: JWT token
N/A
{}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/plans/
POST
admin: JWT token
{
"plan_name": "string",
"description": "string",
"price": float,
"length": int,
"product_limit": int
}
{
"plan_name": "string",
"description": "string",
"price": float,
"length": int,
"product_limit": int
}
{
"error": "bad request: body missing information"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/plans/<int:id>
GET
admin: JWT token
or acc.owner: JWT token
N/A
{
"plan_name": "string",
"description": "string",
"price": float,
"length": int,
"product_limit": int
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/plans/user/<int:id>
GET
admin: JWT token
or acc.owner: JWT token
N/A
{
//entry 1
"plan_name": "string",
"description": "string",
"price": float,
"length": int,
"product_limit": int
},
{
//entry 2... etc
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/plans/
GET
admin: JWT token
N/A
{
//entry 1
"plan_name": "string",
"description": "string",
"price": float,
"length": int,
"product_limit": int
},
{
//entry 2... etc
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/plans/<int:id>
PUT
admin: JWT token
{
"plan_name": "string",
"description": "string",
"price": float,
"length": int,
"product_limit": int
}
{
"plan_name": "string",
"description": "string",
"price": float,
"length": int,
"product_limit": int
}
{
"error": "bad request: body missing information"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/products/
POST
admin: JWT token
{
"product_name": "string",
"description": "string"
}
{
"product_name": "string",
"id": int,
"description": "string"
}
{
"error": "bad request: body missing information"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/products/<int:id>
GET
admin: JWT token
or acc.owner: JWT token
N/A
{
"product_name": "string",
"id": int,
"description": "string"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/products/
GET
admin: JWT token
or acc.owner: JWT token
N/A
{
//entry 1
"product_name": "string",
"id": int,
"description": "string"
},
{
//entry 2... etc
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/products/<int:id>
PUT
admin: JWT token
{
"product_name": "string",
"description": "string"
}
{
"product_name": "string",
"id": int,
"description": "string"
}
{
"error": "bad request: body missing information"
}
{
"error": "not authorised to access this resource"
}
{
"error": "resource not found"
}
/subscription-details/<int:id>
GET
N/A
{
"id": int,
"license": "string",
"product": {
"description": "string",
"id": int,
"product_name": "string"
},
"subscription": {
"end_date": date,
"id": int,
"plan": {
"description": "string",
"id": int,
"length": int,
"plan_name": "string",
"price": float,
"product_limit": int
},
"plan_id": int,
"start_date": "string",
"status": bool,
"user": {
"id": int
}
}
}
{
"error": "bad request: body missing information"
}
{
"error": "field missing information"
}
/subscription-details/subscription/<int:id>
GET
N/A
{
//entry 1
"id": int,
"license": "string",
"product": {
"description": "string",
"id": int,
"product_name": "string"
},
"subscription": {
"end_date": date,
"id": int,
"plan": {
"description": "string",
"id": int,
"length": int,
"plan_name": "string",
"price": float,
"product_limit": int
},
"plan_id": int,
"start_date": "string",
"status": bool,
"user": {
"id": int
}
}
},
{
//entry 2... etc
}
{
"error": "bad request: body missing information"
}
{
"error": "field missing information"
}
/subscription-details/
GET
N/A
{
//entry 1
"id": int,
"license": "string",
"product": {
"description": "string",
"id": int,
"product_name": "string"
},
"subscription": {
"end_date": date,
"id": int,
"plan": {
"description": "string",
"id": int,
"length": int,
"plan_name": "string",
"price": float,
"product_limit": int
},
"plan_id": int,
"start_date": "string",
"status": bool,
"user": {
"id": int
}
}
},
{
//entry 2... etc
}
{
"error": "bad request: body missing information"
}
{
"error": "field missing information"
}