Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions services/form/.env.example

This file was deleted.

4 changes: 4 additions & 0 deletions services/form/app/services/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
"""Business logic layer."""

from app.services.submission_service import SubmissionService

__all__ = ["SubmissionService"]
242 changes: 242 additions & 0 deletions services/form/app/services/submission_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
"""Submission service layer for managing form submissions."""

from datetime import datetime, timezone

from bson import ObjectId
from motor.motor_asyncio import AsyncIOMotorDatabase
from pymongo.errors import PyMongoError

from app.models.common import PyObjectId
from app.models.form import FormInDB
from app.models.submission import SubmissionCreate, SubmissionInDB
from app.utils.logger import logger


class SubmissionService:
"""
Service layer for Submission operations.
Handles business logic and database interactions for submissions.
"""

def __init__(self, db: AsyncIOMotorDatabase):
self.db = db
self.collection = db["submissions"]
self.forms_collection = db["forms"]

def _to_object_id(self, obj_id: PyObjectId) -> ObjectId:
"""
Convert PyObjectId to ObjectId for MongoDB queries.

Args:
obj_id: PyObjectId (which is Annotated[ObjectId, ...])

Returns:
ObjectId for MongoDB operations
"""
if isinstance(obj_id, ObjectId):
Comment thread
DogukanUrker marked this conversation as resolved.
return obj_id
if isinstance(obj_id, str):
return ObjectId(obj_id)
return obj_id

def _document_to_submission(self, doc: dict | None) -> SubmissionInDB | None:
"""
Convert MongoDB document to SubmissionInDB model.

Args:
doc: MongoDB document dict or None

Returns:
SubmissionInDB instance or None
"""
if doc is None:
return None

try:
return SubmissionInDB.model_validate(doc)
except Exception as e:
logger.error(f"Error converting document to SubmissionInDB: {e}")
raise

async def _validate_form(self, form_id: PyObjectId) -> FormInDB:
"""
Validate form exists, is active, and within date range.

Args:
form_id: Form ID to validate

Returns:
FormInDB instance if valid

Raises:
ValueError: If form not found, inactive, not started yet, or deadline passed
"""
object_id = self._to_object_id(form_id)

try:
form_doc = await self.forms_collection.find_one({"_id": object_id})
except PyMongoError as e:
logger.error(f"Database error while validating form {form_id}: {e}")
raise

if form_doc is None:
raise ValueError(f"Form not found with id: {form_id}")

try:
form = FormInDB.model_validate(form_doc)
except Exception as e:
logger.error(f"Error converting form document to FormInDB: {e}")
raise ValueError(f"Invalid form data for id: {form_id}")

if not form.is_active:
raise ValueError("Form is not active")

now = datetime.now(timezone.utc)

# Handle timezone-naive datetimes from MongoDB
if form.start_date is not None:
start_date = form.start_date
if start_date.tzinfo is None:
start_date = start_date.replace(tzinfo=timezone.utc)
if start_date > now:
raise ValueError("Form has not started yet")

if form.deadline is not None:
deadline = form.deadline
if deadline.tzinfo is None:
deadline = deadline.replace(tzinfo=timezone.utc)
if deadline < now:
raise ValueError("Form deadline has passed")

return form

async def create_submission(
self, submission_data: SubmissionCreate
) -> SubmissionInDB:
"""
Create a new submission.

Args:
submission_data: Submission data to create

Returns:
Created SubmissionInDB instance

Raises:
ValueError: For validation errors (form not found, inactive, not started, deadline passed)
PyMongoError: For database errors
"""
logger.info(f"Creating submission for form {submission_data.form_id}")

# Validate form
await self._validate_form(submission_data.form_id)

# Prepare submission document
submission_doc = submission_data.model_dump(by_alias=True, exclude={"id"})
submission_doc["_id"] = ObjectId()
submission_doc["submitted_at"] = datetime.now(timezone.utc)
# Ensure form_id is stored as ObjectId, not string
submission_doc["form_id"] = self._to_object_id(submission_data.form_id)

object_id = self._to_object_id(submission_data.form_id)

try:
# Insert submission
await self.collection.insert_one(submission_doc)

# Atomically increment form's submission_count
await self.forms_collection.update_one(
{"_id": object_id}, {"$inc": {"submission_count": 1}}
)

logger.info(
f"Successfully created submission {submission_doc['_id']} "
f"for form {submission_data.form_id}"
)

return self._document_to_submission(submission_doc)

except PyMongoError as e:
logger.error(f"Database error while creating submission: {e}")
raise

async def get_submission_by_id(
self, submission_id: PyObjectId
) -> SubmissionInDB | None:
"""
Get submission by ID.

Args:
submission_id: Submission ID

Returns:
SubmissionInDB instance if found, None otherwise

Raises:
PyMongoError: For database errors
"""
object_id = self._to_object_id(submission_id)

try:
doc = await self.collection.find_one({"_id": object_id})
Comment thread
DogukanUrker marked this conversation as resolved.
return self._document_to_submission(doc)
except PyMongoError as e:
logger.error(
f"Database error while getting submission {submission_id}: {e}"
)
raise

async def get_submissions_by_form_id(
self, form_id: PyObjectId, skip: int = 0, limit: int = 10
) -> tuple[list[SubmissionInDB], int]:
"""
Get submissions by form ID with pagination.

Args:
form_id: Form ID
skip: Number of documents to skip (default: 0)
limit: Maximum number of documents to return (default: 10, max: 100)

Returns:
Tuple of (list of SubmissionInDB, total count)

Raises:
ValueError: For invalid pagination parameters
PyMongoError: For database errors
"""
# Validate pagination parameters
if skip < 0:
raise ValueError("skip must be >= 0")
if limit < 1 or limit > 100:
raise ValueError("limit must be between 1 and 100")

object_id = self._to_object_id(form_id)

try:
# Count total submissions
total_count = await self.collection.count_documents({"form_id": object_id})

# Query with pagination
cursor = (
self.collection.find({"form_id": object_id})
.sort("submitted_at", -1)
.skip(skip)
.limit(limit)
)

docs = await cursor.to_list(length=limit)

submissions = []
for doc in docs:
if doc is not None:
submission = self._document_to_submission(doc)
if submission is not None:
submissions.append(submission)

return submissions, total_count

except PyMongoError as e:
logger.error(
f"Database error while getting submissions for form {form_id}: {e}"
)
raise