diff --git a/Changelog.md b/Changelog.md
index abf10fcfcb23ac39ed7d51ac1655acb3b4b52ace..7945de885d49a323e82d380a7af0c8220a56db3a 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,9 +1,16 @@
 # Changelog
 
-## [0.3] - 2025-02-13
+## [0.5] - 2025-03-14
+- Improved CSV eport functionality
+- Added Google authentication support
+- Set up PstgreSQL as the default DB
+- Imporved file export name
+- Added unittests
+
+## [0.4] - 2025-02-13
 - Added CSV Export and Modify Functionality
 
-## [0.2] - 2025-01-27
+## [0.3] - 2025-01-27
 - Added new validations
 
 ## [0.2] - 2025-01-09
diff --git a/pyproject.toml b/pyproject.toml
index 491a227d2953598e0456db844b6983c61e7fa0db..4650bd82d4c36d53aca91084f0f6a36f424b402b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,4 +37,10 @@ line-length = 120
 [tool.ruff.lint.flake8-tidy-imports]
 ban-relative-imports = "all"
 
+[tool.ruff.lint.per-file-ignores]
+"test/*" = ["ARG001", "D", "S101", "PLR2004", "PLR0917", "PLR0914", "PLC0415", "PLC2701"]
 
+[tool.pytest.ini_options]
+DJANGO_SETTINGS_MODULE = "test.settings"
+django_find_project = false
+python_files = ["tests.py", "test_*.py", "*_tests.py"]
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 0fb6979ad1f0d490d1ea74e2384a40dd911b001c..a1b9380c20310dd608358bc08ff1c634d5cc92c9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,15 @@
-Django==5.0.11
+Django>=5.0,<5.1
+djangorestframework==3.15.2
 ruff
 mypy
 tox
 sphinx
 sphinx-autodoc-typehints
 mssql-django
+pytest
+pytest-django
+pytest-mock
+faker
+coverage
+social-auth-app-django==5.4.3
+psycopg[binary,pool]
\ No newline at end of file
diff --git a/sage_validation/accounts/__init__.py b/sage_validation/accounts/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/sage_validation/accounts/admin.py b/sage_validation/accounts/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae0e150fb8b9da27994b7720a6e93785ac716426
--- /dev/null
+++ b/sage_validation/accounts/admin.py
@@ -0,0 +1,13 @@
+"""Admin configuration for the UserActivityLog model."""
+from django.contrib import admin
+
+from sage_validation.accounts.models import UserActivityLog
+
+
+@admin.register(UserActivityLog)
+class UserActivityLogAdmin(admin.ModelAdmin):
+    """Admin configuration for the UserActivityLog model."""
+
+    list_display = ("user", "action", "name", "input_file_hash", "output_file_hash", "timestamp")
+    search_fields = ("user__username", "name", "action")
+    list_filter = ("action", "timestamp")
diff --git a/sage_validation/accounts/apps.py b/sage_validation/accounts/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..97f433049e1d7e1c46c6d8bb42fa7f41213cb535
--- /dev/null
+++ b/sage_validation/accounts/apps.py
@@ -0,0 +1,9 @@
+"""Django app configuration for the accounts' app."""
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+    """App configuration for the accounts' app."""
+
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "sage_validation.accounts"
diff --git a/sage_validation/accounts/migrations/0001_initial.py b/sage_validation/accounts/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..163c095f3f495857b641c7615319b1e8ec05e749
--- /dev/null
+++ b/sage_validation/accounts/migrations/0001_initial.py
@@ -0,0 +1,55 @@
+# Generated by Django 5.0.11 on 2025-03-03 09:09
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="UserActivityLog",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "action",
+                    models.CharField(
+                        choices=[("upload", "Upload"), ("download", "Download")],
+                        max_length=10,
+                    ),
+                ),
+                ("name", models.CharField(max_length=255)),
+                (
+                    "input_file_hash",
+                    models.CharField(blank=True, max_length=64, null=True),
+                ),
+                (
+                    "output_file_hash",
+                    models.CharField(blank=True, max_length=64, null=True),
+                ),
+                ("timestamp", models.DateTimeField(auto_now_add=True)),
+                (
+                    "user",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="activity_logs",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+        ),
+    ]
diff --git a/sage_validation/accounts/migrations/__init__.py b/sage_validation/accounts/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/sage_validation/accounts/models.py b/sage_validation/accounts/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..40c3631433f95a58212f49014681ff6c53a75753
--- /dev/null
+++ b/sage_validation/accounts/models.py
@@ -0,0 +1,46 @@
+"""Models for the accounts app."""
+import hashlib
+from typing import ClassVar
+
+from django.contrib.auth.models import User
+from django.db import models
+
+
+class UserActivityLog(models.Model):
+    """Model to log user activities."""
+
+    ACTION_CHOICES: ClassVar[list[tuple[str, str]]] = [
+        ("upload", "Upload"),
+        ("download", "Download"),
+    ]
+
+    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="activity_logs")
+    action = models.CharField(max_length=10, choices=ACTION_CHOICES)
+    name = models.CharField(max_length=255)
+    input_file_hash = models.CharField(max_length=64, blank=True, null=True)
+    output_file_hash = models.CharField(max_length=64, blank=True, null=True)
+    timestamp = models.DateTimeField(auto_now_add=True)
+
+    def __str__(self) -> str:
+        """Return a string representation of the UserActivityLog object."""
+        return f"{self.user.username} - {self.action} - {self.name} ({self.timestamp})"
+
+    @staticmethod
+    def generate_file_hash(file_obj: object) -> str:
+        """Generate SHA-256 hash for a file-like object."""
+        hasher = hashlib.sha256()
+
+        if hasattr(file_obj, "chunks"):
+            for chunk in file_obj.chunks():
+                hasher.update(chunk)
+        elif isinstance(file_obj, str):
+            hasher.update(file_obj.encode("utf-8"))
+
+        elif isinstance(file_obj, list):
+            hasher.update(str(file_obj).encode("utf-8"))
+
+        else:
+            err = "generate_file_hash() expected a file, string, or list."
+            raise TypeError(err)
+
+        return hasher.hexdigest()
diff --git a/sage_validation/accounts/urls.py b/sage_validation/accounts/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..d2b6eada72dd075d384255e9b6bc90975172bc96
--- /dev/null
+++ b/sage_validation/accounts/urls.py
@@ -0,0 +1,7 @@
+"""URL configuration for the accounts' app."""
+from django.contrib.auth.views import LogoutView
+from django.urls import path
+
+urlpatterns = [
+    path("logout/", LogoutView.as_view(), name="logout"),
+]
diff --git a/sage_validation/accounts/views.py b/sage_validation/accounts/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..73cfcebfbe2f5309c824cbc422921dd3d897e64d
--- /dev/null
+++ b/sage_validation/accounts/views.py
@@ -0,0 +1 @@
+"""Views for accounts app."""
diff --git a/sage_validation/file_validator/templates/upload.html b/sage_validation/file_validator/templates/upload.html
index 4a595da4794ec9281508073a490bc54daa4549f5..a9428905a06c54a7c936c2d18c6bcddee3c6d32a 100644
--- a/sage_validation/file_validator/templates/upload.html
+++ b/sage_validation/file_validator/templates/upload.html
@@ -40,21 +40,18 @@
         </div>
     </div>
 
-
     <script>
-        const form = document.getElementById('uploadForm');
-        const fileInput = document.getElementById('fileInput');
-        const errorSection = document.getElementById('errorSection');
-        const errorList = document.getElementById('errorList');
-        const successSection = document.getElementById('successSection');
-        const successMessage = document.getElementById('successMessage');
-        const downloadSection = document.getElementById('downloadSection');
-        const downloadLink = document.getElementById('downloadLink');
-
-        form.addEventListener('submit', async function (e) {
+        document.getElementById('uploadForm').addEventListener('submit', async function (e) {
             e.preventDefault();
 
-            // Clear previous messages
+            const fileInput = document.getElementById('fileInput');
+            const errorSection = document.getElementById('errorSection');
+            const errorList = document.getElementById('errorList');
+            const successSection = document.getElementById('successSection');
+            const successMessage = document.getElementById('successMessage');
+            const downloadSection = document.getElementById('downloadSection');
+            const downloadLink = document.getElementById('downloadLink');
+
             errorList.innerHTML = '';
             successMessage.innerHTML = '';
             errorSection.classList.add('hidden');
@@ -65,7 +62,7 @@
             formData.append('file', fileInput.files[0]);
 
             try {
-                const response = await fetch('', {
+                const response = await fetch("{% url 'upload-file' %}", {
                     method: 'POST',
                     body: formData,
                     headers: {
@@ -80,26 +77,14 @@
                     successSection.classList.remove('hidden');
                     downloadLink.href = result.download_url;
                     downloadSection.classList.remove('hidden');
-                } else if (response.status === 400 && result.status === 'error') {
+                } else {
                     errorList.innerHTML = '';
 
-                    if (Array.isArray(result.errors)) {
-                        result.errors.forEach(errorObj => {
-                            if (typeof errorObj === 'string') {
-                                const li = document.createElement('li');
-                                li.textContent = errorObj;
-                                errorList.appendChild(li);
-                            } else {
-                                for (const [field, messages] of Object.entries(errorObj)) {
-                                    messages.forEach(message => {
-                                        const li = document.createElement('li');
-                                        li.textContent = `${field}: ${message}`;
-                                        errorList.appendChild(li);
-                                    });
-                                }
-                            }
-                        });
-                    } else if (typeof result.errors === 'object') {
+                    if (response.status === 403) {
+                        const li = document.createElement('li');
+                        li.textContent = 'You are not authorized to perform this action.';
+                        errorList.appendChild(li);
+                    } else if (response.status === 400 && result.status === 'error') {
                         for (const [field, messages] of Object.entries(result.errors)) {
                             messages.forEach(message => {
                                 const li = document.createElement('li');
@@ -109,7 +94,7 @@
                         }
                     } else {
                         const li = document.createElement('li');
-                        li.textContent = result.errors;
+                        li.textContent = 'An unexpected error occurred. Please try again.';
                         errorList.appendChild(li);
                     }
 
@@ -117,7 +102,7 @@
                 }
             } catch (error) {
                 const li = document.createElement('li');
-                li.textContent = 'An unexpected error occurred. Please try again.';
+                li.textContent = 'Failed to connect to the server. Please check your internet connection.';
                 errorList.appendChild(li);
                 errorSection.classList.remove('hidden');
             }
diff --git a/sage_validation/file_validator/tests.py b/sage_validation/file_validator/tests.py
deleted file mode 100644
index 676ebd605102ad796947e51e5e7b946f30420eb3..0000000000000000000000000000000000000000
--- a/sage_validation/file_validator/tests.py
+++ /dev/null
@@ -1 +0,0 @@
-"""All the tests for the file_validator app."""
diff --git a/sage_validation/file_validator/urls.py b/sage_validation/file_validator/urls.py
index a5c0d9fd7840097233aabed1184a392037d7bcc1..ee5ae78157bbb92dcb77a93df310e41755f46739 100644
--- a/sage_validation/file_validator/urls.py
+++ b/sage_validation/file_validator/urls.py
@@ -2,9 +2,10 @@
 
 from django.urls import path
 
-from sage_validation.file_validator.views import CSVExportView, CSVUploadView
+from sage_validation.file_validator.views import CSVExportAPIView, CSVUploadAPIView, upload_page_view
 
 urlpatterns = [
-    path("upload/", CSVUploadView.as_view(), name="upload-file"),
-    path("export/", CSVExportView.as_view(), name="export-file"),
+    path("upload-page/", upload_page_view, name="upload-page"),
+    path("api/upload/", CSVUploadAPIView.as_view(), name="upload-file"),
+    path("api/export/", CSVExportAPIView.as_view(), name="export-file"),
 ]
diff --git a/sage_validation/file_validator/views.py b/sage_validation/file_validator/views.py
index 99efd175b0fa9498eb15127578cd85e272e21ac1..5eb98dddf158d5597bdad04b41b256d7b491aaad 100644
--- a/sage_validation/file_validator/views.py
+++ b/sage_validation/file_validator/views.py
@@ -1,15 +1,19 @@
 """Views for the file_validator app."""
 import csv
 import io
-from typing import Any
+from typing import ClassVar
 
-from django.http import HttpRequest, HttpResponse, JsonResponse
+from django.http import HttpRequest, HttpResponse
 from django.shortcuts import render
 from django.urls import reverse_lazy
 from django.utils import timezone
-from django.views.generic.base import View
-from django.views.generic.edit import FormView
+from rest_framework import status
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.request import Request
+from rest_framework.response import Response
+from rest_framework.views import APIView
 
+from sage_validation.accounts.models import UserActivityLog
 from sage_validation.file_validator.forms import CSVUploadForm
 from sage_validation.file_validator.models import MeoCostCentres, XxData
 
@@ -19,46 +23,44 @@ def index_view(request: HttpRequest) -> HttpResponse:
     return render(request, "index.html")
 
 
-class CSVUploadView(FormView):
-    """View for uploading a CSV file."""
+def upload_page_view(request: HttpRequest) -> HttpResponse:
+    """Render the file upload page."""
+    return render(request, "upload.html")
 
-    template_name = "upload.html"
-    form_class = CSVUploadForm
-    success_url = reverse_lazy("upload-file")
 
-    def get_context_data(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
-        """Render the form with no error message on GET request."""
-        context = super().get_context_data(**kwargs)
-        context["error"] = None
-        context["message"] = None
-        return context
+class CSVUploadAPIView(APIView):
+    """API view for uploading a CSV file."""
+
+    permission_classes: ClassVar[list] = [IsAuthenticated]
+
+    def post(self, request: Request) -> Response:
+        """Handle CSV upload and validation."""
+        form = CSVUploadForm(data=request.data, files=request.FILES)
+
+        if not form.is_valid():
+            return Response({"status": "error", "errors": form.errors}, status=status.HTTP_400_BAD_REQUEST)
 
-    def form_valid(self, form: CSVUploadForm) -> JsonResponse:
-        """Handle the CSV validation, store valid data, and prepare for export."""
         csv_file = form.cleaned_data["file"]
         csv_file.seek(0)
         decoded_file = csv_file.read().decode("utf-8").strip()
 
         if not decoded_file:
-            return JsonResponse({"status": "error", "message": "Uploaded file is empty."}, status=400)
+            return Response({"status": "error", "message": "Uploaded file is empty."},
+                            status=status.HTTP_400_BAD_REQUEST)
 
         reader = csv.DictReader(io.StringIO(decoded_file))
         csv_data: list[dict[str, str]] = list(reader)
 
         updated_data = self.update_fields(csv_data)
+        request.session["validated_csv"] = updated_data
+        request.session["input_file_hash"] = UserActivityLog.generate_file_hash(csv_file)
+        request.session.modified = True
 
-        self.request.session["validated_csv"] = updated_data
-        self.request.session.modified = True
-
-        return JsonResponse({
+        return Response({
             "status": "success",
             "message": "File successfully uploaded and processed.",
             "download_url": reverse_lazy("export-file")
-        })
-
-    def form_invalid(self, form: CSVUploadForm) -> JsonResponse:
-        """Handle the form when it is invalid."""
-        return JsonResponse({"status": "error", "errors": form.errors}, status=400)
+        }, status=status.HTTP_200_OK)
 
     @staticmethod
     def update_fields(csv_data: list[dict[str, str]]) -> list[dict[str, str]]:
@@ -91,27 +93,36 @@ class CSVUploadView(FormView):
                     row[f"NominalAnalysisNominalAccountNumber/{repeat}"] = (
                         xx_data[0] if cc_type == "Project" else xx_data[1]
                     )
-
                 repeat += 1
 
         return csv_data
 
 
-class CSVExportView(View):
-    """View for exporting the updated CSV file."""
+class CSVExportAPIView(APIView):
+    """API view for exporting the updated CSV file."""
 
-    def get(self, request: HttpRequest) -> HttpResponse:
-        """Generate a downloadable CSV file with updated values."""
+    def get(self, request: Request) -> Response:
+        """Return processed CSV as a downloadable response."""
         csv_data: list[dict[str, str]] = request.session.get("validated_csv", [])
+        input_file_hash: str = request.session.get("input_file_hash", "")
 
         if not csv_data:
-            return HttpResponse("No data available for export.", status=400)
-
+            return Response({"status": "error", "message": "No data available for export."},
+                            status=status.HTTP_400_BAD_REQUEST)
+        file_name = f"Validated_{csv_data[0].get('TransactionReference', 'file')}.csv"
         response = HttpResponse(content_type="text/csv")
-        response["Content-Disposition"] = 'attachment; filename="updated_file.csv"'
+        response["Content-Disposition"] = f"attachment; filename={file_name}"
 
         writer = csv.DictWriter(response, fieldnames=csv_data[0].keys())
         writer.writeheader()
         writer.writerows(csv_data)
-
+        # Log the user activity
+        UserActivityLog.objects.create(
+            user=request.user,
+            action="download",
+            name=file_name,
+            input_file_hash=input_file_hash,
+            output_file_hash=UserActivityLog.generate_file_hash(csv_data),
+            timestamp=timezone.now()
+        )
         return response
diff --git a/sage_validation/settings.py b/sage_validation/settings.py
index 1a00d60d4de5129a8f1a28d43bc7160f5cea9272..904d0878f92be419b7995d2a83da0634a06a57d3 100644
--- a/sage_validation/settings.py
+++ b/sage_validation/settings.py
@@ -9,6 +9,7 @@ For the full list of settings and their values, see
 https://docs.djangoproject.com/en/5.1/ref/settings/
 """
 import os
+from datetime import timedelta
 from pathlib import Path
 
 # Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -18,7 +19,7 @@ BASE_DIR = Path(__file__).resolve().parent
 # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
 
 # SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = os.getenv("SECRET_KEY")
+SECRET_KEY = os.getenv("SECRET_KEY", "test-secret-key")
 
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = True
@@ -27,6 +28,9 @@ ALLOWED_HOSTS: list[str] = os.getenv("ALLOWED_HOSTS", "").split(",")
 
 # Application definition
 INSTALLED_APPS = [
+    "rest_framework",
+    "rest_framework.authtoken",
+    "social_django",
     "django.contrib.admin",
     "django.contrib.auth",
     "django.contrib.contenttypes",
@@ -36,6 +40,7 @@ INSTALLED_APPS = [
 ]
 LOCAL_APPS = [
     "sage_validation.file_validator",
+    "sage_validation.accounts",
 ]
 THIRD_PARTY_APPS: list[str] = []
 
@@ -48,6 +53,7 @@ MIDDLEWARE = [
     "django.middleware.csrf.CsrfViewMiddleware",
     "django.contrib.auth.middleware.AuthenticationMiddleware",
     "django.contrib.messages.middleware.MessageMiddleware",
+    "social_django.middleware.SocialAuthExceptionMiddleware",
     "django.middleware.clickjacking.XFrameOptionsMiddleware",
 ]
 
@@ -64,6 +70,8 @@ TEMPLATES = [
                 "django.template.context_processors.request",
                 "django.contrib.auth.context_processors.auth",
                 "django.contrib.messages.context_processors.messages",
+                "social_django.context_processors.backends",
+                "social_django.context_processors.login_redirect",
             ],
         },
     },
@@ -76,20 +84,24 @@ WSGI_APPLICATION = "sage_validation.wsgi.application"
 
 DATABASES = {
     "default": {
-        "ENGINE": "django.db.backends.sqlite3",
-        "NAME": BASE_DIR / "db.sqlite3",
+        "ENGINE": "django.db.backends.postgresql",
+        "NAME": os.getenv("POSTGRES_DB_NAME", "sage_validation_db"),
+        "USER": os.getenv("POSTGRES_DB_USER", "sage_user"),
+        "PASSWORD": os.getenv("POSTGRES_DB_PASSWORD", "sage_password"),
+        "HOST": os.getenv("POSTGRES_DB_HOST", "localhost"),
+        "PORT": os.getenv("POSTGRES_DB_PORT", "5432"),
     },
     "meo": {
-        'ENGINE': 'mssql',
-        'NAME': os.getenv("MSSQL_DB_NAME", ""),
-        'USER': os.getenv("MSSQL_DB_USER", ""),
-        'PASSWORD': os.getenv("MSSQL_DB_PASSWORD", ""),
-        'HOST': os.getenv("MSSQL_DB_HOST", "localhost"),
-        'PORT': os.getenv("MSSQL_DB_PORT", ""),
+        "ENGINE": "mssql",
+        "NAME": os.getenv("MSSQL_DB_NAME", ""),
+        "USER": os.getenv("MSSQL_DB_USER", ""),
+        "PASSWORD": os.getenv("MSSQL_DB_PASSWORD", ""),
+        "HOST": os.getenv("MSSQL_DB_HOST", "localhost"),
+        "PORT": os.getenv("MSSQL_DB_PORT", ""),
 
-        'OPTIONS': {
-            'driver': 'ODBC Driver 18 for SQL Server',
-            'extra_params': 'TrustServerCertificate=Yes;'
+        "OPTIONS": {
+            "driver": "ODBC Driver 18 for SQL Server",
+            "extra_params": "TrustServerCertificate=Yes;"
 
         },
     },
@@ -143,6 +155,19 @@ STATICFILES_DIRS = [
 ]
 
 MEDIA_ROOT = os.getenv("MEDIA_ROOT", BASE_DIR / "media")
-MEDIA_URL = '/media/'
+MEDIA_URL = "/media/"
 
 CSRF_TRUSTED_ORIGINS = os.getenv("CSRF_TRUSTED_ORIGINS", "").split(",")
+AUTHENTICATION_BACKENDS = (
+    "social_core.backends.google.GoogleOAuth2",
+    "django.contrib.auth.backends.ModelBackend",
+)
+
+SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = os.getenv("SOCIAL_AUTH_GOOGLE_OAUTH2_KEY", "")
+SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = os.getenv("SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET", "")
+SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ["email", "profile"]
+SOCIAL_AUTH_GOOGLE_OAUTH2_REDIRECT_URI = os.getenv("SOCIAL_AUTH_GOOGLE_OAUTH2_REDIRECT_URI", "")
+SOCIAL_AUTH_JSONFIELD_ENABLED = True
+SOCIAL_AUTH_URL_NAMESPACE = "social"
+LOGIN_REDIRECT_URL = "/"
+LOGOUT_REDIRECT_URL = "/"
\ No newline at end of file
diff --git a/sage_validation/templates/base.html b/sage_validation/templates/base.html
index 20f3c23d95c69128f00d91f7a9e74061ec522528..71daa8e5dee039043a047c18e787b54780444c5f 100644
--- a/sage_validation/templates/base.html
+++ b/sage_validation/templates/base.html
@@ -18,7 +18,18 @@
             <a href="{% url "index" %}" class="text-white text-2xl font-bold tracking-wide">Sage Validation</a> <!-- Made title clickable -->
         </div>
         <div class="flex items-center"> <!-- Login link -->
-            <a href="/login" class="text-white hover:text-gray-300">Login</a>
+            {% if user.is_authenticated %}
+                <p class="text-white">Welcome, {{ user.username }}!</p>
+                <form action="{% url 'logout' %}" method="post" class="inline-block ml-4">
+                    {% csrf_token %}
+                    <button type="submit"
+                            class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 focus:outline-none focus:ring focus:ring-red-400">
+                        Logout
+                    </button>
+                </form>
+            {% else %}
+                <a href="{% url "social:begin" "google-oauth2" %}" class="text-white hover:text-gray-300">Login</a>
+            {% endif %}
             <button id="mobile-menu-button" class="md:hidden text-white focus:outline-none ml-4">
                 <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                     <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path>
diff --git a/sage_validation/templates/index.html b/sage_validation/templates/index.html
index 7d7376ea866409f6da728b1c6a343fa98b8fb7a4..72db6b8b5598643485a9e64dcfd7d9b10aa1f65b 100644
--- a/sage_validation/templates/index.html
+++ b/sage_validation/templates/index.html
@@ -8,7 +8,7 @@
         <h1 class="text-5xl md:text-6xl font-bold mb-12 text-blue-900">Welcome to Sage Validation</h1>
         <p class="text-xl md:text-2xl mb-16 text-gray-700">Click the button below to upload your file for validation.</p>
 
-        <a href="{% url "upload-file" %}"
+        <a href="{% url "upload-page" %}"
            class="inline-flex py-4 px-16 bg-blue-600 text-white text-lg md:text-xl font-bold rounded-full shadow-lg transition-transform transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-blue-300 focus:ring-opacity-50">
             Upload File
         </a>
diff --git a/sage_validation/urls.py b/sage_validation/urls.py
index af6d6158c155dd64a21c7170bb90f05d1fc7020d..1146896db11939a8c6ddc3db541093f717b62166 100644
--- a/sage_validation/urls.py
+++ b/sage_validation/urls.py
@@ -9,5 +9,6 @@ urlpatterns = [
     path("admin/", admin.site.urls),
     path("file-validator/", include("sage_validation.file_validator.urls")),
     path("", index_view, name="index"),
-
+    path("", include("social_django.urls", namespace="social")),
+    path("accounts/", include("sage_validation.accounts.urls")),
 ]
diff --git a/setup.py b/setup.py
index 09c3d78cb76669e38b66c2433c81b9f3ece4497f..f4ffd2f16271a63edda19ec5a7923d13cb73ac28 100644
--- a/setup.py
+++ b/setup.py
@@ -3,12 +3,15 @@ from setuptools import find_packages, setup
 
 setup(
     name="sage-validation",
-    version="0.4",
+    version="0.5",
     packages=find_packages(),
     include_package_data=True,
     install_requires=[
-        "Django==5.0.11",
+        "Django>=5.0,<5.1",
         "mssql-django==1.5",
+        "djangorestframework==3.15.2",
+        "social-auth-app-django==5.4.3",
+        "psycopg[binary,pool]",
     ],
     extras_require={
         "prod": [
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/test/conftest.py b/test/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..94a62ed9ada92e49af102a8213b29e3866eb9b90
--- /dev/null
+++ b/test/conftest.py
@@ -0,0 +1,154 @@
+"""Fixtures for the sage_validation tests."""
+from unittest.mock import MagicMock
+
+import pytest
+from django.contrib.auth.models import User
+from django.core.files.uploadedfile import SimpleUploadedFile
+from faker import Faker
+from rest_framework.test import APIClient
+
+from sage_validation.file_validator.models import MeoCostCentres, MeoValidSuppliers, XxData
+
+
+@pytest.fixture
+def sample_input_file() -> SimpleUploadedFile:
+    """Create a sample valid CSV file for testing."""
+    csv_headers_list = [
+        "AccountNumber",
+        "CBAccountNumber",
+        "DaysDiscountValid",
+        "DiscountValue",
+        "DiscountPercentage",
+        "DueDate",
+        "GoodsValueInAccountCurrency",
+        "PurControlValueInBaseCurrency",
+        "DocumentToBaseCurrencyRate",
+        "DocumentToAccountCurrencyRate",
+        "PostedDate",
+        "QueryCode",
+        "TransactionReference",
+        "SecondReference",
+        "Source",
+        "SYSTraderTranType",
+        "TransactionDate",
+        "UniqueReferenceNumber",
+        "UserNumber",
+        "TaxValue",
+        "SYSTraderGenerationReasonType",
+        "GoodsValueInBaseCurrency",
+        "TransactionReference",
+
+        # NominalAnalysis repeating columns (Example: /1 for first occurrence)
+        "NominalAnalysisTransactionValue/1",
+        "NominalAnalysisNominalAccountNumber/1",
+        "NominalAnalysisNominalCostCentre/1",
+        "NominalAnalysisNominalDepartment/1",
+        "NominalAnalysisNominalAnalysisNarrative/1",
+        "NominalAnalysisTransactionAnalysisCode/1",
+
+        # TaxAnalysis repeating columns (Example: /1 for first occurrence)
+        "TaxAnalysisTaxRate/1",
+        "TaxAnalysisGoodsValueBeforeDiscount/1",
+        "TaxAnalysisDiscountValue/1",
+        "TaxAnalysisDiscountPercentage/1",
+        "TaxAnalysisTaxOnGoodsValue/1",
+    ]
+    csv_headers = ",".join(csv_headers_list)
+
+    csv_content_list = [
+        "12345",  # AccountNumber
+        "54321",  # CBAccountNumber
+        "30",  # DaysDiscountValid
+        "10.5",  # DiscountValue
+        "5.0",  # DiscountPercentage
+        "2024-02-10",  # DueDate
+        "1000",  # GoodsValueInAccountCurrency
+        "950",  # PurControlValueInBaseCurrency
+        "1.2",  # DocumentToBaseCurrencyRate
+        "1.1",  # DocumentToAccountCurrencyRate
+        "2024-02-05",  # PostedDate
+        "Q1",  # QueryCode
+        "TRX123",  # TransactionReference
+        "SREF123",  # SecondReference
+        "80",  # Source
+        "4",  # SYSTraderTranType
+        "2024-02-01",  # TransactionDate
+        "UR123",  # UniqueReferenceNumber
+        "42",  # UserNumber
+        "10",  # TaxValue
+        "1000",  # SYSTraderGenerationReasonType
+        "1200",  # GoodsValueInBaseCurrency
+        "BK123",  # TransactionReference(Batch Number)
+
+        # NominalAnalysis repeating values (Example: /1)
+        "500.75",  # NominalAnalysisTransactionValue/1
+        "ACC100",  # NominalAnalysisNominalAccountNumber/1
+        "CC100",  # NominalAnalysisNominalCostCentre/1
+        "DEP200",  # NominalAnalysisNominalDepartment/1
+        "Sample Narrative",  # NominalAnalysisNominalAnalysisNarrative/1
+        "TAC100",  # NominalAnalysisTransactionAnalysisCode/1
+
+        # TaxAnalysis repeating values (Example: /1)
+        "20.5",  # TaxAnalysisTaxRate/1
+        "900",  # TaxAnalysisGoodsValueBeforeDiscount/1
+        "30",  # TaxAnalysisDiscountValue/1
+        "3.5",  # TaxAnalysisDiscountPercentage/1
+        "180",  # TaxAnalysisTaxOnGoodsValue/1
+    ]
+    csv_content = ",".join(csv_content_list)
+
+    return SimpleUploadedFile("test.csv", f"{csv_headers}\n{csv_content}".encode(), content_type="text/csv")
+
+@pytest.fixture
+def mock_meo_database(mocker: MagicMock)-> None:
+    """Mock the meo database since it's read-only."""
+    fake = Faker()
+
+    # Mock MeoValidSuppliers
+    supplier_mock = MagicMock()
+    supplier_mock.all.return_value = [
+        MeoValidSuppliers(
+            supplier_account_number=str(fake.random_int(min=10000, max=99999)), supplier_account_name=fake.company()
+        ),
+        MeoValidSuppliers(supplier_account_number="12345", supplier_account_name="Sample Narrative")
+    ]
+    mocker.patch("sage_validation.file_validator.models.MeoValidSuppliers.objects.using", return_value=supplier_mock)
+
+    # Mock MeoCostCentres
+    cost_centre_mock = MagicMock()
+    cost_centre_mock.all.return_value = [
+        MeoCostCentres(cc="CC100", cc_type="Project", cc_name="CostCentreName", id=1),
+        MeoCostCentres(cc="CC200", cc_type="Overhead", cc_name="OverheadName", id=2),
+        MeoCostCentres(cc="CC300", cc_type="Overhead", cc_name="DepartmentName", id=3)
+    ]
+    mocker.patch("sage_validation.file_validator.models.MeoCostCentres.objects.using", return_value=cost_centre_mock)
+
+    # Mock XxData
+    xx_data_mock = MagicMock()
+    xx_data_mock.all.return_value = [
+        XxData(xx_value="N100", project="ProjectCode", overhead="OverheadCode", description=fake.sentence()),
+        XxData(xx_value="N200", project="ProjectCode", overhead="OverheadCode", description=fake.sentence())
+    ]
+    mocker.patch("sage_validation.file_validator.models.XxData.objects.using", return_value=xx_data_mock)
+
+    # Mock MeoValidSageAccounts
+    sage_account_mock = MagicMock()
+    sage_account_mock.filter.return_value.exists.return_value = True
+    mocker.patch(
+        "sage_validation.file_validator.models.MeoValidSageAccounts.objects.using", return_value=sage_account_mock
+    )
+
+    # Mock MeoNominal
+    nominal_mock = MagicMock()
+    nominal_mock.filter.return_value.exists.return_value = True
+    mocker.patch("sage_validation.file_validator.models.MeoNominal.objects.using", return_value=nominal_mock)
+
+
+@pytest.fixture
+def api_client() -> APIClient:
+    """Fixture to return Django API test client."""
+    fake = Faker()
+    user = User.objects.create_user(username=fake.user_name(), password=fake.password())
+    client = APIClient()
+    client.force_authenticate(user=user)
+    return client
diff --git a/test/settings.py b/test/settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6f0429ddc29bffad9f2a5b59aa91de9b6850b1a
--- /dev/null
+++ b/test/settings.py
@@ -0,0 +1,15 @@
+"""Settings for running tests."""
+
+from sage_validation.settings import *  # noqa: F403
+
+if os.getenv("USE_SQLITE", "false").lower() == "true":  # noqa: F405
+    DATABASES = {
+        "default": {
+            "ENGINE": "django.db.backends.sqlite3",
+            "NAME": "sage_validation",
+        }
+    }
+else:
+    DATABASES = {
+        "default": DATABASES["default"],
+    }
diff --git a/test/test_file_validator/__init__.py b/test/test_file_validator/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/test/test_file_validator/test_file_validator_endpoints.py b/test/test_file_validator/test_file_validator_endpoints.py
new file mode 100644
index 0000000000000000000000000000000000000000..1d3b79f811465f9ee982d0554d467270ff4941e4
--- /dev/null
+++ b/test/test_file_validator/test_file_validator_endpoints.py
@@ -0,0 +1,90 @@
+from unittest.mock import MagicMock
+
+import pytest
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.urls.base import reverse
+from rest_framework.test import APIClient
+
+from sage_validation.accounts.models import UserActivityLog
+
+UPLOAD_FILE_URL = reverse("upload-file")
+
+
+@pytest.mark.django_db
+def test_csv_upload_unauthenticated(sample_input_file: SimpleUploadedFile) -> None:
+    """Test that a valid CSV upload succeeds."""
+    api_client = APIClient()
+    response = api_client.post(UPLOAD_FILE_URL, {"file": sample_input_file}, format="multipart")
+
+    assert response.status_code == 403
+    assert response.json()["detail"] == "Authentication credentials were not provided."
+
+@pytest.mark.django_db
+def test_csv_upload_valid(
+        api_client: APIClient, sample_input_file: SimpleUploadedFile, mock_meo_database: MagicMock
+) -> None:
+    """Test that a valid CSV upload succeeds."""
+    response = api_client.post(UPLOAD_FILE_URL, {"file": sample_input_file}, format="multipart")
+
+    assert response.status_code == 200
+    assert response.json()["status"] == "success"
+    assert "download_url" in response.json()
+
+
+@pytest.mark.django_db
+def test_csv_upload_invalid_extension(api_client: APIClient) -> None:
+    """Test that uploading a non-CSV file fails."""
+    bad_file = SimpleUploadedFile("test.txt", b"Invalid content", content_type="text/plain")
+
+    response = api_client.post(UPLOAD_FILE_URL, {"file": bad_file}, format="multipart")
+
+    assert response.status_code == 400
+    assert response.json()["status"] == "error"
+    assert "errors" in response.json()
+
+
+@pytest.mark.django_db
+def test_csv_export_with_data(api_client: APIClient) -> None:
+    """Test exporting a processed CSV file."""
+    url = reverse("export-file")
+
+    # Simulate session data
+    session = api_client.session
+    session["validated_csv"] = [
+        {"AccountNumber": "12345", "TransactionDate": "01/03/2024", "TransactionReference": "BK1234"},
+    ]
+    session["input_file_hash"] = "123456"
+
+    session.save()
+
+    response = api_client.get(url)
+
+    assert UserActivityLog.objects.count() == 1
+    assert response.status_code == 200
+    assert response["Content-Disposition"] == "attachment; filename=Validated_BK1234.csv"
+    assert b"AccountNumber,TransactionDate,TransactionReference" in response.content
+    assert b"12345,01/03/2024,BK1234" in response.content
+
+
+@pytest.mark.django_db
+def test_activity_log_creation_on_csv_export(api_client: APIClient) -> None:
+    """Test that a UserActivityLog is created when exporting a CSV file."""
+    url = reverse("export-file")
+
+    # Simulate session data
+    session = api_client.session
+    session["validated_csv"] = [
+        {"AccountNumber": "12345", "TransactionDate": "01/03/2024", "TransactionReference": "BK1234"},
+    ]
+    session["input_file_hash"] = "123456"
+
+    session.save()
+
+    response = api_client.get(url)
+
+    assert response.status_code == 200
+    log = UserActivityLog.objects.first()
+    assert log.action == "download"
+    assert log.input_file_hash == "123456"
+    assert log.output_file_hash == UserActivityLog.generate_file_hash(session["validated_csv"])
+    assert log.name == "Validated_BK1234.csv"
diff --git a/test/test_file_validator/test_forms.py b/test/test_file_validator/test_forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..b5df1089c1430e9a898b62cd8449ef849415f422
--- /dev/null
+++ b/test/test_file_validator/test_forms.py
@@ -0,0 +1,94 @@
+"""Tests for the file_validator forms."""
+import csv
+import io
+from unittest.mock import MagicMock
+
+from django.core.files.uploadedfile import SimpleUploadedFile
+
+from sage_validation.file_validator.forms import CSVUploadForm
+
+
+def create_modified_csv(sample_file: SimpleUploadedFile, modifications: dict[str, str]) -> SimpleUploadedFile:
+    """
+    Modify specific fields in the first row of a CSV file and return a new SimpleUploadedFile.
+
+    Args:
+        sample_file (SimpleUploadedFile): The original CSV file.
+        modifications (dict): Dictionary of column names to modified values.
+
+    Returns:
+        SimpleUploadedFile: The modified CSV file.
+    """
+    csv_content = sample_file.read().decode("utf-8").splitlines()
+    reader = csv.DictReader(csv_content)
+    rows = list(reader)
+
+    for key, value in modifications.items():
+        rows[0][key] = value
+
+    output = io.StringIO()
+    writer = csv.DictWriter(output, fieldnames=reader.fieldnames or [])
+    writer.writeheader()
+    writer.writerows(rows)
+
+    return SimpleUploadedFile("test_modified.csv", output.getvalue().encode("utf-8"), content_type="text/csv")
+
+
+def test_valid_csv_upload(sample_input_file: SimpleUploadedFile, mock_meo_database: MagicMock) -> None:
+    """Test CSV upload with valid data."""
+    form = CSVUploadForm(files={"file": sample_input_file})
+    assert form.is_valid(), f"Form errors: {form.errors}"
+
+
+def test_invalid_file_extension() -> None:
+    """Test form rejects non-CSV files."""
+    bad_file = SimpleUploadedFile("test.txt", b"Some text content", content_type="text/plain")
+    form = CSVUploadForm(files={"file": bad_file})
+    assert not form.is_valid()
+    assert "File must be in CSV format." in form.errors["file"]
+
+
+def test_missing_required_columns() -> None:
+    """Test form rejects CSV missing required headers."""
+    invalid_csv = SimpleUploadedFile("test.csv", b"AccountNumber,CBAccountNumber\n12345,54321", content_type="text/csv")
+    form = CSVUploadForm(files={"file": invalid_csv})
+    assert not form.is_valid()
+    assert "Missing required columns" in str(form.errors)
+
+
+def test_source_and_trader_type_validation(sample_input_file: SimpleUploadedFile, mock_meo_database: MagicMock) -> None:
+    """Test validation for Source and SYSTraderTranType columns."""
+    modified_file = create_modified_csv(sample_input_file, {"Source": "90", "SYSTraderTranType": "5"})
+    form = CSVUploadForm(files={"file": modified_file})
+    assert not form.is_valid()
+    assert "Row 1: 'Source' must be 80" in form.errors["file"][0]
+    assert "Row 1: 'SYSTraderTranType' must be 4" in form.errors["file"][1]
+
+
+def test_validate_nominal_analysis_account(sample_input_file: SimpleUploadedFile, mock_meo_database: MagicMock) -> None:
+    """Test validation for nominal analysis account."""
+    modified_file = create_modified_csv(sample_input_file,
+                                        {"NominalAnalysisNominalAnalysisNarrative/1": "Invalid Name"})
+    form = CSVUploadForm(files={"file": modified_file})
+    assert not form.is_valid()
+    assert form.errors["file"][0] == (
+        "Row 1: 'AccountNumber' must match 'Sample Narrative' in 'NominalAnalysisNominalAnalysisNarrative/1'"
+        ", but found 'Invalid Name'.")
+
+
+def test_validate_nc_cc_dep_combination_against_meo_sage_account(
+        sample_input_file: SimpleUploadedFile, mock_meo_database: MagicMock
+) -> None:
+    """Test validation for nominal analysis fields against MEO valid Sage accounts."""
+    modified_file = create_modified_csv(
+        sample_input_file,
+        {
+            "NominalAnalysisNominalCostCentre/1": "Invalid_CC",
+            "NominalAnalysisNominalAccountNumber/1": "Invalid_Account"
+        })
+
+    form = CSVUploadForm(files={"file": modified_file})
+    assert not form.is_valid()
+    assert ("Row 1: 'NominalAnalysisNominalCostCentre/1' (Invalid_CC) is not a valid cost centre."
+            in str(form.errors["file"][0]))
+
diff --git a/tox.ini b/tox.ini
index 91d0161e6590165ecf51bec47ab84ff68718aacd..1db7951f22971976c85cee2b4ecf9259839e4724 100644
--- a/tox.ini
+++ b/tox.ini
@@ -5,6 +5,17 @@ envlist = py311
 deps =
     mypy
     ruff
+    -r requirements.txt
+
+setenv =
+    USE_SQLITE = true
+
 commands =
     ruff check .
     mypy .
+    coverage erase
+    coverage run --source sage_validation -m pytest
+    coverage report --fail-under=90
+    coverage xml
+    coverage html
+