Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b5dbc6a
feat: add .dockerignore to reduce build context size
codcreater1 Jul 2, 2026
c0fd62b
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 2, 2026
58cc3fb
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 2, 2026
a7a0d8c
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 2, 2026
5662ffd
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 2, 2026
fc424cf
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 2, 2026
4feb80f
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 2, 2026
01225e0
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 2, 2026
cdb378b
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 2, 2026
b779c47
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 2, 2026
1165274
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 7, 2026
0d3b748
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 14, 2026
1445cee
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 14, 2026
b66b82a
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 16, 2026
7cdb654
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 16, 2026
1127d6d
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 16, 2026
9526e90
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 16, 2026
02f25f4
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 16, 2026
2f1ed63
Merge branch 'main' of https://github.com/codcreater1/agentic-interns…
codcreater1 Jul 20, 2026
8f330ba
fix: stamp the real signing date next to the signature
codcreater1 Jul 21, 2026
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
7 changes: 6 additions & 1 deletion backend/app/routers/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from app.services import application_repository as repo
from app.services import email_service
from app.services.application_service import ApplicationService
from app.services.contract_service import ContractService
from app.services.contract_service import SIGNATURE_DATE_POS, ContractService
from app.services.pdf_service import pdf_service
from app.services.storage_service import storage_service
from app.services.token_service import create_download_token
Expand Down Expand Up @@ -144,6 +144,10 @@ def sign_application(index: int, request: SignApplicationRequest):

signed_path = source_pdf.parent / "signed.pdf"

# Stamp the date the coordinator actually signs, not the date the contract
# was generated (which can be days earlier).
signed_on = datetime.now(timezone.utc).date().isoformat()

pdf_service.embed_signature(
source_pdf=source_pdf,
output_pdf=signed_path,
Expand All @@ -153,6 +157,7 @@ def sign_application(index: int, request: SignApplicationRequest):
y=request.y,
w=request.w,
h=request.h,
text_stamps=[(signed_on, *SIGNATURE_DATE_POS)],
)

token = create_download_token(application.contract_task_id)
Expand Down
10 changes: 9 additions & 1 deletion backend/app/services/contract_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
_BEAR_BROWN = HexColor("#7A5A2E")
_PAGE_W, _PAGE_H = A4

# Where the signing date is stamped, in PDF points with a TOP-LEFT origin (the
# convention pdf_service uses). Mirrors the reportlab position of the date line
# below, which is drawn at 61 mm from the bottom.
SIGNATURE_DATE_POS = (_PAGE_W - 73 * mm, _PAGE_H - 63 * mm)


class ContractService:
@staticmethod
Expand Down Expand Up @@ -109,9 +114,12 @@ def field(label: str, value: str):
c.line(left, 61 * mm, left + 70 * mm, 61 * mm) # signature line
c.drawString(left, 55 * mm, "Signature of the Dean's Internship Supervisor")

# The date beside the signature is intentionally left blank here: it is
# the date the coordinator signs, which happens later (often days after
# the contract is generated). It is stamped during signing, at
# SIGNATURE_DATE_POS below.
c.drawString(_PAGE_W - 85 * mm, 61 * mm, "Date:")
c.line(_PAGE_W - 73 * mm, 61 * mm, _PAGE_W - 20 * mm, 61 * mm)
c.drawString(_PAGE_W - 73 * mm, 63 * mm, date.today().isoformat())

c.setFont("Helvetica-Oblique", 7)
c.drawString(left, 15 * mm,
Expand Down
12 changes: 12 additions & 0 deletions backend/app/services/pdf_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ def embed_signature(
y: float,
w: float,
h: float,
text_stamps: list[tuple[str, float, float]] | None = None,
) -> None:
"""Embed *image_bytes* on *page_index* of *source_pdf*, save to *output_pdf*.

Expand All @@ -204,6 +205,10 @@ def embed_signature(
using page top-left as origin with y growing downward.
w, h — box width and height in PDF points (both must be > 0).

text_stamps — optional (text, x, y) items drawn on the same page in the
same save, for values that are only known at signing time (the
signing date). Same top-left origin as above.

Save options (garbage=3, deflate=True, clean=True):
- Drop orphan objects and duplicate streams (garbage=3).
- zlib-compress all streams (deflate=True).
Expand Down Expand Up @@ -250,6 +255,13 @@ def embed_signature(
f"PyMuPDF rejected the signature image: {exc}"
) from exc

for text, tx, ty in (text_stamps or []):
try:
page.insert_text((tx, ty), text, fontsize=10, fontname="helv")
except (ValueError, RuntimeError) as exc:
# A stamp is supplementary — never lose the signature over it.
logger.warning("could not stamp %r: %s", text, exc)

try:
doc.save(
str(part_path),
Expand Down
39 changes: 39 additions & 0 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,42 @@ def test_sign_pdf_download_url_is_reachable():
download = client.get(download_url)
assert download.status_code == 200
assert download.headers["content-type"] == "application/pdf"


def test_signature_date_is_stamped_at_signing_not_generation(tmp_path):
"""The date beside the signature must be the date the coordinator signed.

It used to be printed by create_contract_pdf, i.e. when the contract was
generated — often days before signing — so a contract signed on the 21st
still showed the 16th. Generation must leave it blank; signing stamps it.
"""
from datetime import datetime, timezone

from app.services.contract_service import SIGNATURE_DATE_POS, ContractService
from app.services.pdf_service import pdf_service

unsigned = tmp_path / "contract.pdf"
ContractService.create_contract_pdf(
name="Ada Lovelace", email="ada@example.com",
recommended_role="Backend Developer Internship",
candidate_score=80, output_path=unsigned,
)

today = datetime.now(timezone.utc).date().isoformat()
unsigned_text = fitz.open(str(unsigned))[0].get_text()
# Only the header "Date:" field carries a date before signing.
assert unsigned_text.count(today) == 1

img = Image.new("RGBA", (60, 25), (0, 0, 255, 255))
buf = io.BytesIO()
img.save(buf, format="PNG")

signed = tmp_path / "signed.pdf"
pdf_service.embed_signature(
source_pdf=unsigned, output_pdf=signed, image_bytes=buf.getvalue(),
page_index=0, x=70, y=600, w=220, h=70,
text_stamps=[(today, *SIGNATURE_DATE_POS)],
)

signed_text = fitz.open(str(signed))[0].get_text()
assert signed_text.count(today) == 2 # header + signing date
Loading