Files
breakpilot-lehrer/school-service/internal/services/certificate_service.go
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

252 lines
10 KiB
Go

package services
import (
"context"
"encoding/json"
"fmt"
"github.com/breakpilot/school-service/internal/models"
"github.com/jackc/pgx/v5/pgxpool"
)
// CertificateService handles certificate-related operations
type CertificateService struct {
db *pgxpool.Pool
gradeService *GradeService
gradebookService *GradebookService
}
// NewCertificateService creates a new CertificateService
func NewCertificateService(db *pgxpool.Pool, gradeService *GradeService, gradebookService *GradebookService) *CertificateService {
return &CertificateService{
db: db,
gradeService: gradeService,
gradebookService: gradebookService,
}
}
// CertificateTemplate represents a certificate template
type CertificateTemplate struct {
Name string `json:"name"`
FederalState string `json:"federal_state"`
SchoolType string `json:"school_type"`
GradeLevel string `json:"grade_level"` // "1-4", "5-10", "11-13"
TemplatePath string `json:"template_path"`
}
// GetAvailableTemplates returns available certificate templates
func (s *CertificateService) GetAvailableTemplates() []CertificateTemplate {
// In a real implementation, these would be loaded from a templates directory
return []CertificateTemplate{
{Name: "Halbjahreszeugnis Grundschule", FederalState: "generic", SchoolType: "grundschule", GradeLevel: "1-4", TemplatePath: "generic/grundschule_halbjahr.html"},
{Name: "Jahreszeugnis Grundschule", FederalState: "generic", SchoolType: "grundschule", GradeLevel: "1-4", TemplatePath: "generic/grundschule_jahr.html"},
{Name: "Halbjahreszeugnis Sek I", FederalState: "generic", SchoolType: "sek1", GradeLevel: "5-10", TemplatePath: "generic/sek1_halbjahr.html"},
{Name: "Jahreszeugnis Sek I", FederalState: "generic", SchoolType: "sek1", GradeLevel: "5-10", TemplatePath: "generic/sek1_jahr.html"},
{Name: "Halbjahreszeugnis Sek II", FederalState: "generic", SchoolType: "sek2", GradeLevel: "11-13", TemplatePath: "generic/sek2_halbjahr.html"},
{Name: "Abiturzeugnis", FederalState: "generic", SchoolType: "sek2", GradeLevel: "11-13", TemplatePath: "generic/abitur.html"},
// Niedersachsen specific
{Name: "Halbjahreszeugnis Gymnasium (NI)", FederalState: "niedersachsen", SchoolType: "gymnasium", GradeLevel: "5-10", TemplatePath: "niedersachsen/gymnasium_halbjahr.html"},
// NRW specific
{Name: "Halbjahreszeugnis Gymnasium (NRW)", FederalState: "nrw", SchoolType: "gymnasium", GradeLevel: "5-10", TemplatePath: "nrw/gymnasium_halbjahr.html"},
}
}
// GenerateCertificate generates a certificate for a student
func (s *CertificateService) GenerateCertificate(ctx context.Context, req *models.GenerateCertificateRequest) (*models.Certificate, error) {
// Get student grades
grades, err := s.gradeService.GetStudentGrades(ctx, req.StudentID)
if err != nil {
return nil, fmt.Errorf("failed to get student grades: %w", err)
}
// Filter grades for the requested school year and semester
var relevantGrades []models.GradeOverview
for _, g := range grades {
if g.SchoolYearID.String() == req.SchoolYearID && g.Semester == req.Semester {
relevantGrades = append(relevantGrades, g)
}
}
// Get attendance summary
excusedDays, unexcusedDays, err := s.gradebookService.GetAttendanceSummary(ctx, req.StudentID, req.SchoolYearID)
if err != nil {
// Non-fatal, continue with zero absences
excusedDays, unexcusedDays = 0, 0
}
// Build grades JSON
gradesMap := make(map[string]interface{})
for _, g := range relevantGrades {
gradesMap[g.SubjectName] = map[string]interface{}{
"written_avg": g.WrittenGradeAvg,
"oral": g.OralGrade,
"final": g.FinalGrade,
"final_locked": g.FinalGradeLocked,
}
}
gradesJSON, _ := json.Marshal(gradesMap)
// Create certificate record
var certificate models.Certificate
err = s.db.QueryRow(ctx, `
INSERT INTO certificates (student_id, school_year_id, semester, certificate_type, template_name, grades_json, remarks, absence_days, absence_days_unexcused, status)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'draft')
RETURNING id, student_id, school_year_id, semester, certificate_type, template_name, grades_json, remarks, absence_days, absence_days_unexcused, generated_pdf_path, status, created_at, updated_at
`, req.StudentID, req.SchoolYearID, req.Semester, req.CertificateType, req.TemplateName, gradesJSON, req.Remarks, excusedDays+unexcusedDays, unexcusedDays).Scan(
&certificate.ID, &certificate.StudentID, &certificate.SchoolYearID, &certificate.Semester, &certificate.CertificateType, &certificate.TemplateName, &certificate.GradesJSON, &certificate.Remarks, &certificate.AbsenceDays, &certificate.AbsenceDaysUnexcused, &certificate.GeneratedPDFPath, &certificate.Status, &certificate.CreatedAt, &certificate.UpdatedAt,
)
if err != nil {
return nil, err
}
return &certificate, nil
}
// GetCertificates returns certificates for a class
func (s *CertificateService) GetCertificates(ctx context.Context, classID string, semester int) ([]models.Certificate, error) {
rows, err := s.db.Query(ctx, `
SELECT c.id, c.student_id, c.school_year_id, c.semester, c.certificate_type, c.template_name, c.grades_json, c.remarks, c.absence_days, c.absence_days_unexcused, c.generated_pdf_path, c.status, c.created_at, c.updated_at,
CONCAT(st.first_name, ' ', st.last_name) as student_name,
cl.name as class_name
FROM certificates c
JOIN students st ON c.student_id = st.id
JOIN classes cl ON st.class_id = cl.id
WHERE cl.id = $1 AND c.semester = $2
ORDER BY st.last_name, st.first_name
`, classID, semester)
if err != nil {
return nil, err
}
defer rows.Close()
var certificates []models.Certificate
for rows.Next() {
var cert models.Certificate
if err := rows.Scan(&cert.ID, &cert.StudentID, &cert.SchoolYearID, &cert.Semester, &cert.CertificateType, &cert.TemplateName, &cert.GradesJSON, &cert.Remarks, &cert.AbsenceDays, &cert.AbsenceDaysUnexcused, &cert.GeneratedPDFPath, &cert.Status, &cert.CreatedAt, &cert.UpdatedAt, &cert.StudentName, &cert.ClassName); err != nil {
return nil, err
}
certificates = append(certificates, cert)
}
return certificates, nil
}
// GetCertificate returns a single certificate
func (s *CertificateService) GetCertificate(ctx context.Context, certificateID string) (*models.Certificate, error) {
var cert models.Certificate
err := s.db.QueryRow(ctx, `
SELECT c.id, c.student_id, c.school_year_id, c.semester, c.certificate_type, c.template_name, c.grades_json, c.remarks, c.absence_days, c.absence_days_unexcused, c.generated_pdf_path, c.status, c.created_at, c.updated_at,
CONCAT(st.first_name, ' ', st.last_name) as student_name,
cl.name as class_name
FROM certificates c
JOIN students st ON c.student_id = st.id
JOIN classes cl ON st.class_id = cl.id
WHERE c.id = $1
`, certificateID).Scan(
&cert.ID, &cert.StudentID, &cert.SchoolYearID, &cert.Semester, &cert.CertificateType, &cert.TemplateName, &cert.GradesJSON, &cert.Remarks, &cert.AbsenceDays, &cert.AbsenceDaysUnexcused, &cert.GeneratedPDFPath, &cert.Status, &cert.CreatedAt, &cert.UpdatedAt, &cert.StudentName, &cert.ClassName,
)
return &cert, err
}
// UpdateCertificate updates a certificate
func (s *CertificateService) UpdateCertificate(ctx context.Context, certificateID string, remarks string) (*models.Certificate, error) {
var cert models.Certificate
err := s.db.QueryRow(ctx, `
UPDATE certificates SET remarks = $2, updated_at = NOW()
WHERE id = $1
RETURNING id, student_id, school_year_id, semester, certificate_type, template_name, grades_json, remarks, absence_days, absence_days_unexcused, generated_pdf_path, status, created_at, updated_at
`, certificateID, remarks).Scan(
&cert.ID, &cert.StudentID, &cert.SchoolYearID, &cert.Semester, &cert.CertificateType, &cert.TemplateName, &cert.GradesJSON, &cert.Remarks, &cert.AbsenceDays, &cert.AbsenceDaysUnexcused, &cert.GeneratedPDFPath, &cert.Status, &cert.CreatedAt, &cert.UpdatedAt,
)
return &cert, err
}
// FinalizeCertificate finalizes a certificate (prevents further changes)
func (s *CertificateService) FinalizeCertificate(ctx context.Context, certificateID string) error {
_, err := s.db.Exec(ctx, `
UPDATE certificates SET status = 'final', updated_at = NOW()
WHERE id = $1
`, certificateID)
return err
}
// GeneratePDF generates a PDF for a certificate
// In a real implementation, this would use a PDF generation library
func (s *CertificateService) GeneratePDF(ctx context.Context, certificateID string) ([]byte, error) {
cert, err := s.GetCertificate(ctx, certificateID)
if err != nil {
return nil, err
}
// Placeholder: In reality, this would:
// 1. Load the HTML template
// 2. Fill in student data, grades, attendance
// 3. Convert to PDF using a library like wkhtmltopdf or chromedp
// For now, return a simple text representation
content := fmt.Sprintf(`
ZEUGNIS
Schüler/in: %s
Klasse: %s
Schuljahr: Halbjahr %d
Typ: %s
Noten:
%v
Fehlzeiten: %d Tage (davon %d unentschuldigt)
Bemerkungen:
%s
Status: %s
`, cert.StudentName, cert.ClassName, cert.Semester, cert.CertificateType, cert.GradesJSON, cert.AbsenceDays, cert.AbsenceDaysUnexcused, cert.Remarks, cert.Status)
return []byte(content), nil
}
// DeleteCertificate deletes a certificate (only if draft)
func (s *CertificateService) DeleteCertificate(ctx context.Context, certificateID string) error {
_, err := s.db.Exec(ctx, `DELETE FROM certificates WHERE id = $1 AND status = 'draft'`, certificateID)
return err
}
// BulkGenerateCertificates generates certificates for all students in a class
func (s *CertificateService) BulkGenerateCertificates(ctx context.Context, classID, schoolYearID string, semester int, certificateType models.CertificateType, templateName string) ([]models.Certificate, error) {
// Get all students in the class
rows, err := s.db.Query(ctx, `SELECT id FROM students WHERE class_id = $1`, classID)
if err != nil {
return nil, err
}
defer rows.Close()
var studentIDs []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
studentIDs = append(studentIDs, id)
}
// Generate certificate for each student
var certificates []models.Certificate
for _, studentID := range studentIDs {
cert, err := s.GenerateCertificate(ctx, &models.GenerateCertificateRequest{
StudentID: studentID,
SchoolYearID: schoolYearID,
Semester: semester,
CertificateType: certificateType,
TemplateName: templateName,
})
if err != nil {
// Log error but continue with other students
continue
}
certificates = append(certificates, *cert)
}
return certificates, nil
}