diff --git a/Makefile b/Makefile
index b6a55edce75790912f8cf2a2db08c48768f90b3c..b5f80cd6a3145449f796c53a5bfd1f73572c5689 100644
--- a/Makefile
+++ b/Makefile
@@ -26,7 +26,7 @@ reset-db:
 	flask --app aocp init-db
 
 .PHONY: test
-test: test-validation test-aoc-py test-verifier
+test: test-validation test-aoc-py test-verifier test-score-py
 
 .PHONY: test-validation
 test-validation:
@@ -35,3 +35,7 @@ test-validation:
 .PHONY: test-aoc-py
 test-aoc-py:
 	flask --app aocp --debug test-aoc-py
+
+.PHONY: test-score-py
+test-score-py:
+	flask --app aocp --debug test-score-py
diff --git a/aocp/__init__.py b/aocp/__init__.py
index 195e0ddf3c5c5f9d5dd2f5d5ea8471af8ef0426b..bc803f38a1ffc7e9607ce483b80a14925c97e677 100644
--- a/aocp/__init__.py
+++ b/aocp/__init__.py
@@ -2,6 +2,8 @@ from . import db
 from . import auth
 from . import verifier
 from . import aoc
+from . import score
+
 
 import os
 import json
@@ -100,4 +102,7 @@ def create_app(test_config=None):
     # Initialize verifier
     verifier.init_app(app)
 
+    # Initialize score
+    score.init_app(app)
+
     return app
diff --git a/aocp/aoc.py b/aocp/aoc.py
index 5337ba94b83dbb105559a0f70f996ceb04167a58..30eed426e241721c1261fa4138bf58798760f077 100755
--- a/aocp/aoc.py
+++ b/aocp/aoc.py
@@ -1,10 +1,10 @@
-from typing import Literal
+from typing import Tuple, cast
 import dateparser
-from datetime import date, datetime
+from datetime import datetime, timezone
+from zoneinfo import ZoneInfo
 from enum import Enum
 from flask import current_app
 import requests
-from flask import app
 import re
 import click
 import requests
@@ -102,3 +102,27 @@ def init_app(app):
     Gets called by __init__.py on initialization of the flask App.
     """
     app.cli.add_command(test_aoc_py_command)
+
+def get_aoc_day(timestamp : datetime = datetime.now(tz=timezone.utc), year : int|None = None)-> Tuple[int, int]:
+    """
+    Returns a tuple:
+    - The current AOC Day (1-25), or 0 if the event hasn't started yet, or 26 if the event is already over.
+    - The hour (0-23) of the AOC Day of the timestamp.
+
+    Parameters:
+    timestamp -- Overwrite the "current" timestamp. Must be timezone aware.
+    year -- Overwrite the AOC year. If None the YEAR from the config is used.
+    """
+    timestamp = timestamp.astimezone(ZoneInfo("EST")) # convert to EST time
+    hour = timestamp.hour
+    if year is None:
+        year = cast(int, current_app.config["YEAR"])
+    if timestamp.year > year:
+        return 26, hour
+    if timestamp.year < year:
+        return 0, hour
+    if timestamp.month < 12:
+        return 0, hour
+    return min(timestamp.day, 26), hour
+
+
diff --git a/aocp/score.py b/aocp/score.py
index 63b55b1db5d33e2803c893b482735008f25bf84b..30097c3292af4dbd94336e2332e3cb242653061f 100644
--- a/aocp/score.py
+++ b/aocp/score.py
@@ -1,48 +1,147 @@
 from datetime import datetime
-from zoneinfo import ZoneInfo
+from . import db as database
+import click
+from . import aoc
+from flask import current_app
+from . import db as database
+from . import verifier
+from typing import Tuple, cast
 
-# timedelta is hour after conversion!
-# return 0 points if not current day
-
-def calculate_points(day:int, timedelta:int, is_first:bool)->int:
+def calculate_points(day:int, timestamp:datetime, is_first:bool)->int:
     """
     Calculates the points based on day, time and if the solver was first.
-    
-    
+
     Parameters:
-    day    -- Day of the solution.
-    timedelta -- Hour of the solution.
-    is_first -- Boolean (True if person was first to solve problem).
-    
+    day       -- Day that the solution was submitted for.
+    timestamp -- Actual timestamp of the solution.
+    is_first  -- Boolean (True if person was first to solve problem on the day).
+
     Returns:
-    score -- the score
+    score -- the score or zero if the submission was made on the wrong day or outside of the event
     """
-    # get "base points" depending on day
-    base_points = 1+ day // 5  # base points increase by 1 every 5 days.
-    # bonus time points: (all ranges inclusive)
-    # 0-8: 2 points
-    # 8-16: 1 point
-    # 16-24: 0 points
-    bonus_time_points = (24-timedelta) // 8 
-    if is_first: # bonus point if person is first to solve on given day.
-        bonus_first = 1
-    else:
-        bonus_first = 0
-    score = base_points + bonus_time_points + bonus_first 
+    sub_day, sub_hour = aoc.get_aoc_day(timestamp)
+
+    if sub_day != day or sub_day <= 0 or sub_day >= 26:
+        return 0 # Submission was made on the wrong day or outside of the event
+
+    score = 0
+
+    # base points by day: (all ranges inclusive)
+    # 1-5: 1 point
+    # 6-10: 2 points
+    # 11-15: 3 points
+    # 16-20: 4 points
+    # 21-25: 5 points
+    score += (day-1) // 5
+
+    # bonus time points by hour since start of day: (all ranges inclusive)
+    # 0-7: 2 points
+    # 8-15: 1 point
+    # 16-23: 0 points
+    score += 2 - (sub_hour // 8)
+
+    # bonus point if person is first to solve on given day.
+    if is_first:
+        score += 1
+
     return score
-    
-def is_first_solve(sub_id:tuple[int,int,int],day:int):
-    # incomplete function 
-    first = get_first(day)
-    return sub_id == first
-
-def get_datetime_info(timestamp:datetime):
-    conv_time = timestamp.astimezone(ZoneInfo("EST")) # convert to EST time
-    day = conv_time.day
-    hour = conv_time.hour
-    return day,hour
-
-def get_first(day):
-    # missing function
-    pass
-    # query all "CORRECT"s of <DAY> and return the SUBMISSION ID first one
+
+def get_first(day:int)->str|None:
+    """
+    takes an AOC day (1-25) as an integer and checks who was the first to solve the puzzle on that day.
+    returns: the userid of the first solver or None if there are no solvers for the given day.
+    """
+    corrects = database.get_db().fetch_submissions(status="CORRECT",day=day)
+    if corrects.empty:
+        return None
+    corrects = corrects.sort_values(by="timestamp")
+    userid = str(corrects.head(1).index[0][0])
+    return userid
+
+def get_leaderboard(groupid:str|None = None)->Tuple[dict,list[dict]]:
+    """
+    Get the compiled data for building the leaderboard.
+
+    Is a user has made no relevant correct submissions, they will not appear in the result.
+
+    Parameters:
+    groupid  -- If set, only fetch the data for building the leaderboard for this group.
+
+    Returns a tuple:
+    - A dict of the solves and score for each person:
+    {
+        uid: {
+            uid:str
+            total:int
+            days:{
+                day:{first:bool,score:int,solved:bool}
+            }
+        }
+    }
+    - The leaderboard as a list of the person dicts sorted by 'total':
+    [
+        {
+            uid:str
+            total:int
+            days:{
+                day:{first:bool,score:int,solved:bool}
+            }
+        },
+        ...
+    ]
+    """
+    solves = {}
+    df = database.get_db().fetch_submissions(status="CORRECT", groupid=groupid)
+    #df = df.sort_values(by=["userid","day"])
+    first_solvers = {day:get_first(day) for day in range(1,26)}
+
+    for index,sub in df.iterrows():
+        (uid,day,_) = cast(tuple,index)
+        first = bool(first_solvers[day] == uid)
+        points = calculate_points(day=day,timestamp=sub.timestamp,is_first=first)
+        #print(uid," ",day,"   ",points)
+        if uid not in solves:
+            solves[uid]={"userid":uid,"total":0,"days":{day:{"first":False,"score":0,"solved":False} for day in range(1,26)}}
+        solves[uid]["days"][day]["score"] = points
+        solves[uid]["days"][day]["first"] = first
+        solves[uid]["days"][day]["solved"] = True
+        solves[uid]["total"] += points
+
+    leaderboard = list(solves.values())
+    leaderboard = sorted(leaderboard, key=lambda d: d['total'])
+
+    return solves,leaderboard
+
+def test_user_submissions():
+    userid_peter, _ = database.get_db().login_or_register_user("123", "peter@example.com")
+    database.get_db().add_submission(userid_peter, 3, "Hallo")
+    database.get_db().add_submission(userid_peter, 3, "Welt")
+    database.get_db().add_submission(userid_peter, 3, "Yeet")
+    database.get_db().add_submission(userid_peter, 3, "123")
+@click.command('test-score-py')
+def test_score_py_command():
+    click.echo('Adding Submissions')
+    #submit_answer(2022,1,1,"30")
+    database.get_db().set_day_puzzle(day=3,puzzle=b'203nsk20')
+    database.get_db().set_day_puzzle(day=7,puzzle=b'2138')
+    database.get_db().set_day_puzzle(day=20,puzzle=b'abcabca')
+    database.get_db().set_day_puzzle(day=1,puzzle=b'aksjndaks')
+    database.get_db().set_day_puzzle(day=21,puzzle=b'test')
+    database.get_db().set_day_puzzle(day=24,puzzle=b'test2')
+    database.get_db().set_day_solution(3,solution="123")
+    database.get_db().set_day_solution(7,solution="13")
+    database.get_db().set_day_solution(20,solution="abcabca")
+    database.get_db().set_day_solution(21,solution="12")
+    database.get_db().set_day_solution(24,solution="10")
+    test_user_submissions()
+    click.echo('Verifying...')
+    current_app.logger.debug(verifier.verify())
+    click.echo('testing get-leaderboard')
+    solves, leaderboard = get_leaderboard()
+    current_app.logger.debug("Solves:")
+    current_app.logger.debug(solves)
+    current_app.logger.debug("Leaderboard:")
+    current_app.logger.debug(leaderboard)
+def init_app(app):
+    app.cli.add_command(test_score_py_command)
+