Compare commits
	
		
			32 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						af587b0ac1
	
				 | 
					
					
						|||
| 
						
						
							
						
						50e3eff294
	
				 | 
					
					
						|||
| 
						
						
							
						
						bc17f7256b
	
				 | 
					
					
						|||
| 
						
						
							
						
						d2f3eea8a5
	
				 | 
					
					
						|||
| 
						
						
							
						
						f902c61490
	
				 | 
					
					
						|||
| 
						
						
							
						
						11e8cc5b1d
	
				 | 
					
					
						|||
| 
						
						
							
						
						84155b7404
	
				 | 
					
					
						|||
| 
						
						
							
						
						45b37197ec
	
				 | 
					
					
						|||
| 
						
						
							
						
						f65848924c
	
				 | 
					
					
						|||
| 
						
						
							
						
						98d584867e
	
				 | 
					
					
						|||
| 
						
						
							
						
						376e8de1a4
	
				 | 
					
					
						|||
| 
						
						
							
						
						2911391fb9
	
				 | 
					
					
						|||
| 
						
						
							
						
						6d2e0241c9
	
				 | 
					
					
						|||
| 
						
						
							
						
						afc5b1f0c6
	
				 | 
					
					
						|||
| 
						
						
							
						
						4a76ee469b
	
				 | 
					
					
						|||
| 
						
						
							
						
						b58bf700df
	
				 | 
					
					
						|||
| 
						
						
							
						
						efd3a35802
	
				 | 
					
					
						|||
| 
						
						
							
						
						0f7e44a42a
	
				 | 
					
					
						|||
| 
						
						
							
						
						f90e5d75fa
	
				 | 
					
					
						|||
| 
						
						
							
						
						31d4ec5f27
	
				 | 
					
					
						|||
| 
						
						
							
						
						d61d4d6e7e
	
				 | 
					
					
						|||
| 
						
						
							
						
						606ce6b940
	
				 | 
					
					
						|||
| 
						
						
							
						
						750fa70332
	
				 | 
					
					
						|||
| 
						
						
							
						
						7d503edbc9
	
				 | 
					
					
						|||
| 
						
						
							
						
						5c9235df8d
	
				 | 
					
					
						|||
| 
						
						
							
						
						11ea0858bb
	
				 | 
					
					
						|||
| 
						
						
							
						
						4d57cf827d
	
				 | 
					
					
						|||
| 
						
						
							
						
						df9f7fdc13
	
				 | 
					
					
						|||
| 
						
						
							
						
						cdd2b5e250
	
				 | 
					
					
						|||
| 
						
						
							
						
						94b766f106
	
				 | 
					
					
						|||
| 
						
						
							
						
						a2e94f715b
	
				 | 
					
					
						|||
| 
						
						
							
						
						f64daaf817
	
				 | 
					
					
						
							
								
								
									
										9
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								.env
									
									
									
									
									
								
							@@ -1,3 +1,4 @@
 | 
			
		||||
LOGLEVEL=debug
 | 
			
		||||
PORT=3000
 | 
			
		||||
PRODUCION=false
 | 
			
		||||
APIKEY=lfk
 | 
			
		||||
@@ -10,9 +11,13 @@ CARD_SUBTITLE=Kaya ist cool
 | 
			
		||||
CARD_BARCODEFORMAT=ean13
 | 
			
		||||
# CARD_BARCODEPREFIX=
 | 
			
		||||
 | 
			
		||||
SPONSOING_RECEIPTMINIMUM=10
 | 
			
		||||
SPONSORING_RECEIPTMINIMUM=40
 | 
			
		||||
SPONSORING_DISCLAIMER=Kaya ist cool, aber pass auf, dass du nicht zu viel Geld sammelst!
 | 
			
		||||
SPONSORING_BARCODEFORMAT=code128
 | 
			
		||||
# SPONSORING_BARCODEPREFIX=
 | 
			
		||||
 | 
			
		||||
CERTIFICATE_FOOTER=Kaya ist cool, danke für deine Unterstützung!
 | 
			
		||||
CERTIFICATE_FOOTER=Kaya ist cool, danke für deine Unterstützung!
 | 
			
		||||
 | 
			
		||||
SEPA_BIC=FNOMDEB2
 | 
			
		||||
SEPA_NAME=ODIT.Services
 | 
			
		||||
SEPA_IBAN=DE25100180000690238989
 | 
			
		||||
							
								
								
									
										27
									
								
								.gitea/workflows/dev.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.gitea/workflows/dev.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
name: Build latest image
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - main
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build-container:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
      - name: Login to registry
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          registry: registry.odit.services
 | 
			
		||||
          username: ${{ vars.REGISTRY_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.REGISTRY_PASSWORD }}
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - name: Build and push
 | 
			
		||||
        uses: docker/build-push-action@v6
 | 
			
		||||
        with:
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: |
 | 
			
		||||
            ${{ vars.REGISTRY }}/lfk/document-server:latest
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
							
								
								
									
										27
									
								
								.gitea/workflows/release.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.gitea/workflows/release.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
name: Build release images
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    tags:
 | 
			
		||||
      - "*.*.*"
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build-container:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
      - name: Login to registry
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          registry: registry.odit.services
 | 
			
		||||
          username: ${{ vars.REGISTRY_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.REGISTRY_PASSWORD }}
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - name: Build and push
 | 
			
		||||
        uses: docker/build-push-action@v6
 | 
			
		||||
        with:
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: |
 | 
			
		||||
            ${{ vars.REGISTRY }}/lfk/document-server:${{ github.ref_name }}
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
steps:
 | 
			
		||||
  - name: build latest
 | 
			
		||||
    image: woodpeckerci/plugin-docker-buildx
 | 
			
		||||
    settings:
 | 
			
		||||
      repo: registry.odit.services/lfk/document-server
 | 
			
		||||
      tags:
 | 
			
		||||
        - latest
 | 
			
		||||
      registry: registry.odit.services
 | 
			
		||||
      platforms: linux/amd64,linux/arm64
 | 
			
		||||
      cache_from: registry.odit.services/lfk/document-server:latest
 | 
			
		||||
      username:
 | 
			
		||||
        from_secret: odit-registry-builder-username
 | 
			
		||||
      password:
 | 
			
		||||
        from_secret: odit-registry-builder-password
 | 
			
		||||
    when:
 | 
			
		||||
      branch: main
 | 
			
		||||
when:
 | 
			
		||||
  event: push
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
steps:
 | 
			
		||||
  - name: build tag
 | 
			
		||||
    image: woodpeckerci/plugin-docker-buildx
 | 
			
		||||
    settings:
 | 
			
		||||
      repo: registry.odit.services/lfk/document-server
 | 
			
		||||
      tags:
 | 
			
		||||
        - "${CI_COMMIT_TAG}"
 | 
			
		||||
      registry: registry.odit.services
 | 
			
		||||
      platforms: linux/amd64,linux/arm64
 | 
			
		||||
      cache_from: registry.odit.services/lfk/document-server:latest
 | 
			
		||||
      username:
 | 
			
		||||
        from_secret: odit-registry-builder-username
 | 
			
		||||
      password:
 | 
			
		||||
        from_secret: odit-registry-builder-password
 | 
			
		||||
when:
 | 
			
		||||
  event:
 | 
			
		||||
    - tag
 | 
			
		||||
							
								
								
									
										11
									
								
								docs/docs.go
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								docs/docs.go
									
									
									
									
									
								
							@@ -326,6 +326,17 @@ const docTemplate = `{
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "name": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "parent_group": {
 | 
			
		||||
                    "type": "object",
 | 
			
		||||
                    "required": [
 | 
			
		||||
                        "name"
 | 
			
		||||
                    ],
 | 
			
		||||
                    "properties": {
 | 
			
		||||
                        "name": {
 | 
			
		||||
                            "type": "string"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -317,6 +317,17 @@
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "name": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "parent_group": {
 | 
			
		||||
                    "type": "object",
 | 
			
		||||
                    "required": [
 | 
			
		||||
                        "name"
 | 
			
		||||
                    ],
 | 
			
		||||
                    "properties": {
 | 
			
		||||
                        "name": {
 | 
			
		||||
                            "type": "string"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -96,6 +96,13 @@ definitions:
 | 
			
		||||
    properties:
 | 
			
		||||
      name:
 | 
			
		||||
        type: string
 | 
			
		||||
      parent_group:
 | 
			
		||||
        properties:
 | 
			
		||||
          name:
 | 
			
		||||
            type: string
 | 
			
		||||
        required:
 | 
			
		||||
        - name
 | 
			
		||||
        type: object
 | 
			
		||||
    required:
 | 
			
		||||
    - name
 | 
			
		||||
    type: object
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								go.mod
									
									
									
									
									
								
							@@ -49,7 +49,8 @@ require (
 | 
			
		||||
	github.com/valyala/fasthttp v1.57.0 // indirect
 | 
			
		||||
	github.com/valyala/tcplisten v1.0.0 // indirect
 | 
			
		||||
	go.uber.org/atomic v1.9.0 // indirect
 | 
			
		||||
	go.uber.org/multierr v1.9.0 // indirect
 | 
			
		||||
	go.uber.org/multierr v1.10.0 // indirect
 | 
			
		||||
	go.uber.org/zap v1.27.0 // indirect
 | 
			
		||||
	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
 | 
			
		||||
	golang.org/x/sys v0.27.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.19.0 // indirect
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							@@ -107,6 +107,10 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 | 
			
		||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 | 
			
		||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
 | 
			
		||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
 | 
			
		||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
 | 
			
		||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 | 
			
		||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 | 
			
		||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 | 
			
		||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
 | 
			
		||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
 | 
			
		||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,8 @@ import (
 | 
			
		||||
//	@Router			/v1/barcodes/{type}/{content} [get]
 | 
			
		||||
func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
 | 
			
		||||
 | 
			
		||||
	logger := h.Logger.Named("GenerateBarcode")
 | 
			
		||||
 | 
			
		||||
	// Get the type and content from the URL
 | 
			
		||||
	barcodeType := c.Params("type")
 | 
			
		||||
	barcodeContent := c.Params("content")
 | 
			
		||||
@@ -32,6 +34,7 @@ func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
 | 
			
		||||
	// Convert width and height to integers
 | 
			
		||||
	width, err := strconv.Atoi(widthStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Invalid width parameter", "width", widthStr, "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 | 
			
		||||
			"error": "Invalid width parameter",
 | 
			
		||||
		})
 | 
			
		||||
@@ -39,6 +42,7 @@ func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
 | 
			
		||||
 | 
			
		||||
	height, err := strconv.Atoi(heightStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Invalid height parameter", "height", heightStr, "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 | 
			
		||||
			"error": "Invalid height parameter",
 | 
			
		||||
		})
 | 
			
		||||
@@ -46,18 +50,23 @@ func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
 | 
			
		||||
 | 
			
		||||
	padding, err := strconv.Atoi(paddingStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Invalid padding parameter", "padding", paddingStr, "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 | 
			
		||||
			"error": "Invalid padding parameter",
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	logger = logger.With("type", barcodeType, "content", barcodeContent, "width", width, "height", height, "padding", padding)
 | 
			
		||||
 | 
			
		||||
	// Generate the barcode
 | 
			
		||||
	logger.Info("Generating barcode")
 | 
			
		||||
	barcode, err := h.BarcodeService.GenerateBarcode(barcodeType, barcodeContent, width, height, padding)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to generate barcode", "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	logger.Info("Barcode generated")
 | 
			
		||||
 | 
			
		||||
	c.Set(fiber.HeaderContentType, "image/png")
 | 
			
		||||
	return c.Send(barcode.Bytes())
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"log"
 | 
			
		||||
	"slices"
 | 
			
		||||
 | 
			
		||||
	"git.odit.services/lfk/document-server/models"
 | 
			
		||||
@@ -19,27 +18,35 @@ import (
 | 
			
		||||
//	@Security		ApiKeyAuth
 | 
			
		||||
//	@Router			/v1/pdfs/cards [post]
 | 
			
		||||
func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error {
 | 
			
		||||
 | 
			
		||||
	logger := h.Logger.Named("GenerateCard")
 | 
			
		||||
 | 
			
		||||
	cardRequest := new(models.CardRequest)
 | 
			
		||||
	if err := c.BodyParser(cardRequest); err != nil {
 | 
			
		||||
		logger.Errorw("Invalid request", "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	if !slices.Contains([]string{"en", "de"}, cardRequest.Locale) {
 | 
			
		||||
		logger.Errorw("Invalid locale", "locale", cardRequest.Locale)
 | 
			
		||||
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 | 
			
		||||
			"error": "Invalid locale",
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger = logger.With("locale", cardRequest.Locale)
 | 
			
		||||
 | 
			
		||||
	templateString, err := h.StaticService.GetTemplate(cardRequest.Locale, "card")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
		logger.Errorw("Template not found", "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 | 
			
		||||
			"error": "Template not found",
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	template, err := h.Templater.StringToTemplate(templateString)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Error parsing template", "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
@@ -53,20 +60,26 @@ func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error {
 | 
			
		||||
		BarcodePrefix: h.Config.CardBarcodePrefix,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Info("Generating card html")
 | 
			
		||||
	result, err := h.Templater.Execute(template, genConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Error executing template", "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	logger.Info("Generated card html")
 | 
			
		||||
	c.Set(fiber.HeaderContentType, "text/html")
 | 
			
		||||
 | 
			
		||||
	logger.Info("Converting html to pdf")
 | 
			
		||||
	pdf, err := h.Converter.ToPdf(result, "a4", false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Error converting html to pdf", "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	logger.Info("Converted html to pdf")
 | 
			
		||||
 | 
			
		||||
	c.Set(fiber.HeaderContentType, "application/pdf")
 | 
			
		||||
	c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-cards.pdf")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"log"
 | 
			
		||||
	"slices"
 | 
			
		||||
 | 
			
		||||
	"git.odit.services/lfk/document-server/models"
 | 
			
		||||
@@ -19,27 +18,35 @@ import (
 | 
			
		||||
//	@Security		ApiKeyAuth
 | 
			
		||||
//	@Router			/v1/pdfs/certificates [post]
 | 
			
		||||
func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
 | 
			
		||||
 | 
			
		||||
	logger := h.Logger.Named("GenerateCertificate")
 | 
			
		||||
 | 
			
		||||
	certificateRequest := new(models.CertificateRequest)
 | 
			
		||||
	if err := c.BodyParser(certificateRequest); err != nil {
 | 
			
		||||
		logger.Errorw("Invalid request", "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	if !slices.Contains([]string{"en", "de"}, certificateRequest.Locale) {
 | 
			
		||||
		logger.Errorw("Invalid locale", "locale", certificateRequest.Locale)
 | 
			
		||||
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 | 
			
		||||
			"error": "Invalid locale",
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger = logger.With("locale", certificateRequest.Locale)
 | 
			
		||||
 | 
			
		||||
	templateString, err := h.StaticService.GetTemplate(certificateRequest.Locale, "certificate")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
		logger.Errorw("Template not found", "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 | 
			
		||||
			"error": "Template not found",
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	template, err := h.Templater.StringToTemplate(templateString)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Error parsing template", "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
@@ -51,22 +58,32 @@ func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
 | 
			
		||||
		Footer:         h.Config.CertificateFooter,
 | 
			
		||||
		CurrencySymbol: h.Config.CurrencySymbol,
 | 
			
		||||
		Locale:         certificateRequest.Locale,
 | 
			
		||||
		SepaConfig: &models.SepaConfig{
 | 
			
		||||
			BIC:                h.Config.SepaBic,
 | 
			
		||||
			HolderName:         h.Config.SepaName,
 | 
			
		||||
			IBAN:               h.Config.SepaIban,
 | 
			
		||||
			CurrencyIdentifier: h.Config.CurrencyIdentifier,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Info("Generating certificate html")
 | 
			
		||||
	result, err := h.Templater.Execute(template, genConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	logger.Info("Generated card html")
 | 
			
		||||
	c.Set(fiber.HeaderContentType, "text/html")
 | 
			
		||||
 | 
			
		||||
	logger.Info("Converting html to pdf")
 | 
			
		||||
	pdf, err := h.Converter.ToPdf(result, "a4", false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	logger.Info("Converted html to pdf")
 | 
			
		||||
 | 
			
		||||
	c.Set(fiber.HeaderContentType, "application/pdf")
 | 
			
		||||
	c.Set(fiber.HeaderContentDisposition, "attachment; filename=certificate.pdf")
 | 
			
		||||
@@ -79,6 +96,13 @@ func addUpRunnerDonations(runners []models.RunnerWithDonations) []models.RunnerW
 | 
			
		||||
			runners[i].TotalDonations += runners[i].DistanceDonations[j].Amount
 | 
			
		||||
			runners[i].TotalPerDistance += runners[i].DistanceDonations[j].AmountPerDistance
 | 
			
		||||
		}
 | 
			
		||||
		if runners[i].Group.ParentGroup.Name != "" {
 | 
			
		||||
			runners[i].CombinedGroupName = runners[i].Group.ParentGroup.Name + " - " + runners[i].Group.Name
 | 
			
		||||
		} else {
 | 
			
		||||
			runners[i].CombinedGroupName = runners[i].Group.Name
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	return runners
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"log"
 | 
			
		||||
	"slices"
 | 
			
		||||
 | 
			
		||||
	"git.odit.services/lfk/document-server/models"
 | 
			
		||||
@@ -19,36 +18,42 @@ import (
 | 
			
		||||
//	@Security		ApiKeyAuth
 | 
			
		||||
//	@Router			/v1/pdfs/contracts [post]
 | 
			
		||||
func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error {
 | 
			
		||||
 | 
			
		||||
	logger := h.Logger.Named("GenerateContract")
 | 
			
		||||
 | 
			
		||||
	contract := new(models.ContractRequest)
 | 
			
		||||
	if err := c.BodyParser(contract); err != nil {
 | 
			
		||||
		logger.Errorw("Invalid request", "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	if !slices.Contains([]string{"en", "de"}, contract.Locale) {
 | 
			
		||||
		logger.Errorw("Invalid locale", "locale", contract.Locale)
 | 
			
		||||
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 | 
			
		||||
			"error": "Invalid locale",
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	contract.Runners = repeatRunnerArrayItems(contract.Runners, 2)
 | 
			
		||||
	logger = logger.With("locale", contract.Locale)
 | 
			
		||||
 | 
			
		||||
	templateString, err := h.StaticService.GetTemplate(contract.Locale, "contract")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
		logger.Errorw("Template not found", "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 | 
			
		||||
			"error": "Template not found",
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	template, err := h.Templater.StringToTemplate(templateString)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Error parsing template", "error", err)
 | 
			
		||||
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	genConfig := &models.ContractTemplateOptions{
 | 
			
		||||
		Runners:              contract.Runners,
 | 
			
		||||
		Runners:              repeatRunnerArrayItems(contract.Runners, 2),
 | 
			
		||||
		CurrencySymbol:       h.Config.CurrencySymbol,
 | 
			
		||||
		Disclaimer:           h.Config.SponosringDisclaimer,
 | 
			
		||||
		ReceiptMinimumAmount: h.Config.SponsoringReceiptMinimum,
 | 
			
		||||
@@ -57,19 +62,23 @@ func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error {
 | 
			
		||||
		BarcodePrefix:        h.Config.SponsoringBarcodePrefix,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Info("Generating contract html")
 | 
			
		||||
	result, err := h.Templater.Execute(template, genConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	logger.Info("Generated contract html")
 | 
			
		||||
 | 
			
		||||
	logger.Info("Converting html to pdf")
 | 
			
		||||
	pdf, err := h.Converter.ToPdf(result, "a5", true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	logger.Info("Converted html to pdf")
 | 
			
		||||
 | 
			
		||||
	c.Set(fiber.HeaderContentType, "application/pdf")
 | 
			
		||||
	c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-contracts.pdf")
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import (
 | 
			
		||||
	"git.odit.services/lfk/document-server/models"
 | 
			
		||||
	"git.odit.services/lfk/document-server/services"
 | 
			
		||||
	"github.com/gofiber/fiber/v2"
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Handler interface {
 | 
			
		||||
@@ -19,4 +20,5 @@ type DefaultHandler struct {
 | 
			
		||||
	Templater      services.Templater
 | 
			
		||||
	Converter      services.Converter
 | 
			
		||||
	StaticService  services.StaticService
 | 
			
		||||
	Logger         *zap.SugaredLogger
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										71
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										71
									
								
								main.go
									
									
									
									
									
								
							@@ -3,7 +3,8 @@ package main
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"crypto/subtle"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"git.odit.services/lfk/document-server/docs" // Correct import path for docs
 | 
			
		||||
	"git.odit.services/lfk/document-server/handlers"
 | 
			
		||||
@@ -12,13 +13,17 @@ import (
 | 
			
		||||
	"github.com/gofiber/fiber/v2"
 | 
			
		||||
	"github.com/gofiber/fiber/v2/middleware/cors"
 | 
			
		||||
	"github.com/gofiber/fiber/v2/middleware/keyauth"
 | 
			
		||||
	"github.com/gofiber/fiber/v2/middleware/requestid"
 | 
			
		||||
	"github.com/gofiber/swagger"
 | 
			
		||||
	"github.com/redis/go-redis/v9"
 | 
			
		||||
	"github.com/spf13/viper"
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
	"go.uber.org/zap/zapcore"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	config *models.Config
 | 
			
		||||
	logger *zap.SugaredLogger
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func validateAPIKey(c *fiber.Ctx, key string) (bool, error) {
 | 
			
		||||
@@ -33,11 +38,13 @@ func validateAPIKey(c *fiber.Ctx, key string) (bool, error) {
 | 
			
		||||
 | 
			
		||||
func loadEnv() error {
 | 
			
		||||
 | 
			
		||||
	viper.SetDefault("PRODUCION", true)
 | 
			
		||||
	viper.SetDefault("LOGLEVEL", "INFO")
 | 
			
		||||
	viper.SetDefault("PRODUCION", false)
 | 
			
		||||
	viper.SetDefault("PORT", "3000")
 | 
			
		||||
	viper.SetDefault("APIKEY", "lfk")
 | 
			
		||||
	viper.SetDefault("EVENTNAME", "Demo Event")
 | 
			
		||||
	viper.SetDefault("CURRENCYSYMBOL", "€")
 | 
			
		||||
	viper.SetDefault("CURRENCYIDENTIFIER", "EUR")
 | 
			
		||||
	viper.SetDefault("CARD_SUBTITLE", "Runner Card")
 | 
			
		||||
	viper.SetDefault("CARD_BARCODEFORMAT", "ean13")
 | 
			
		||||
	viper.SetDefault("CARD_BARCODEPREFIX", "")
 | 
			
		||||
@@ -48,6 +55,9 @@ func loadEnv() error {
 | 
			
		||||
	viper.SetDefault("CERTIFICATE_FOOTER", "Footer")
 | 
			
		||||
	viper.SetDefault("GOTENBERG_BASEURL", "")
 | 
			
		||||
	viper.SetDefault("REDIS_ADDR", "")
 | 
			
		||||
	viper.SetDefault("SEPA_BIC", "")
 | 
			
		||||
	viper.SetDefault("SEPA_NAME", "")
 | 
			
		||||
	viper.SetDefault("SEPA_IBAN", "")
 | 
			
		||||
 | 
			
		||||
	// Load .env file
 | 
			
		||||
	viper.SetConfigFile(".env")
 | 
			
		||||
@@ -56,7 +66,7 @@ func loadEnv() error {
 | 
			
		||||
	viper.AutomaticEnv()
 | 
			
		||||
	err := viper.ReadInConfig()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println("No .env file found")
 | 
			
		||||
		logger.Warn("No .env file found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Unmarshal the config from file and env into the config struct
 | 
			
		||||
@@ -65,7 +75,34 @@ func loadEnv() error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Printf("Loaded config: %+v\n", config)
 | 
			
		||||
	logger.Infow("Loaded config", "config", &config)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initLogger() error {
 | 
			
		||||
	logLevel := os.Getenv("LOGLEVEL")
 | 
			
		||||
	if logLevel == "" {
 | 
			
		||||
		logLevel = "INFO"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var zapLogLevel zapcore.Level
 | 
			
		||||
	err := zapLogLevel.UnmarshalText([]byte(strings.ToLower(logLevel)))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		zapLogLevel = zapcore.InfoLevel
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	zapConfig := zap.NewProductionConfig()
 | 
			
		||||
	zapConfig.Level = zap.NewAtomicLevelAt(zapLogLevel)
 | 
			
		||||
	zapLogger, err := zapConfig.Build()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer zapLogger.Sync()
 | 
			
		||||
	logger = zapLogger.Sugar()
 | 
			
		||||
 | 
			
		||||
	logger.Debug("Initialized logger")
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -82,14 +119,21 @@ func loadEnv() error {
 | 
			
		||||
// @name						key
 | 
			
		||||
func main() {
 | 
			
		||||
 | 
			
		||||
	err := loadEnv()
 | 
			
		||||
	// Init the logger
 | 
			
		||||
	err := initLogger()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = loadEnv()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var redisClient *redis.Client
 | 
			
		||||
	if config.RedisAddr != "" {
 | 
			
		||||
		log.Println("Using redis at", config.RedisAddr)
 | 
			
		||||
		logger.Infow("Using redis", "redisAddr", config.RedisAddr)
 | 
			
		||||
		redisClient = redis.NewClient(&redis.Options{
 | 
			
		||||
			Addr: config.RedisAddr,
 | 
			
		||||
		})
 | 
			
		||||
@@ -97,9 +141,11 @@ func main() {
 | 
			
		||||
 | 
			
		||||
	barcodeGenerator := &services.DefaultBarcodeService{
 | 
			
		||||
		RedisClient: redisClient,
 | 
			
		||||
		Logger:      logger.Named("DefaultBarcodeService"),
 | 
			
		||||
	}
 | 
			
		||||
	staticService := &services.DefaultStaticService{
 | 
			
		||||
		Cache: make(map[string]string),
 | 
			
		||||
		Cache:  make(map[string]string),
 | 
			
		||||
		Logger: logger.Named("DefaultStaticService"),
 | 
			
		||||
	}
 | 
			
		||||
	handler := handlers.DefaultHandler{
 | 
			
		||||
		Config:         config,
 | 
			
		||||
@@ -108,11 +154,15 @@ func main() {
 | 
			
		||||
		Templater: &services.DefaultTemplater{
 | 
			
		||||
			BarcodeService: barcodeGenerator,
 | 
			
		||||
			StaticService:  staticService,
 | 
			
		||||
			Logger:         logger.Named("DefaultTemplater"),
 | 
			
		||||
		},
 | 
			
		||||
		Converter: &services.GotenbergConverter{
 | 
			
		||||
			BaseUrl: config.GotenbergBaseUrl,
 | 
			
		||||
			Logger:  logger.Named("GotenbergConverter"),
 | 
			
		||||
		},
 | 
			
		||||
		Logger: logger.Named("DefaultHandler"),
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debug("Initialized services")
 | 
			
		||||
 | 
			
		||||
	// Create a new Fiber instance
 | 
			
		||||
	app := fiber.New(fiber.Config{
 | 
			
		||||
@@ -120,6 +170,7 @@ func main() {
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	app.Use(cors.New())
 | 
			
		||||
	app.Use(requestid.New())
 | 
			
		||||
 | 
			
		||||
	// Swagger documentation route
 | 
			
		||||
	app.Get("/swagger/*", swagger.HandlerDefault)
 | 
			
		||||
@@ -137,9 +188,11 @@ func main() {
 | 
			
		||||
	pdfv1.Post("/certificates", handler.GenerateCertificate)
 | 
			
		||||
 | 
			
		||||
	v1.Get("/barcodes/:type/:content", handler.GenerateBarcode)
 | 
			
		||||
	logger.Debug("Initialized routes")
 | 
			
		||||
 | 
			
		||||
	app.Use(handler.NotFoundHandler)
 | 
			
		||||
	docs.SwaggerInfo.BasePath = "/"
 | 
			
		||||
 | 
			
		||||
	log.Fatal(app.Listen("0.0.0.0:" + config.Port))
 | 
			
		||||
	logger.Infow("Starting server", "port", config.Port)
 | 
			
		||||
	logger.Error(app.Listen("0.0.0.0:" + config.Port))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,10 +11,12 @@ type RunnerWithDonations struct {
 | 
			
		||||
	MiddleName        string             `json:"middle_name" validate:"optional"`
 | 
			
		||||
	LastName          string             `json:"last_name" validate:"required"`
 | 
			
		||||
	Group             Group              `json:"group" validate:"required"`
 | 
			
		||||
	CombinedGroupName string             `json:"combined_group_name" validate:"optional"`
 | 
			
		||||
	Distance          int                `json:"distance" validate:"required"`
 | 
			
		||||
	DistanceDonations []DistanceDonation `json:"distance_donations" validate:"optional"`
 | 
			
		||||
	TotalPerDistance  int                `json:"total_per_distance" validate:"optional"`
 | 
			
		||||
	TotalDonations    int                `json:"total_donations" validate:"optional"`
 | 
			
		||||
	SelfServiceLink   string             `json:"self_service_link" validate:"required"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DistanceDonation struct {
 | 
			
		||||
@@ -38,4 +40,12 @@ type CertificateTemplateOptions struct {
 | 
			
		||||
	Footer         string                `json:"footer"`
 | 
			
		||||
	CurrencySymbol string                `json:"currency_symbol"`
 | 
			
		||||
	Locale         string                `json:"locale"`
 | 
			
		||||
	SepaConfig     *SepaConfig           `json:"sepa_config"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SepaConfig struct {
 | 
			
		||||
	IBAN               string `json:"iban" validate:"required"`
 | 
			
		||||
	HolderName         string `json:"holder_name" validate:"required"`
 | 
			
		||||
	BIC                string `json:"bic" validate:"required"`
 | 
			
		||||
	CurrencyIdentifier string `json:"currency_identifier" validate:"required"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,24 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	LogLevel                 string `mapstructure:"LOGLEVEL"`
 | 
			
		||||
	Prod                     bool   `mapstructure:"PRODUCION"`
 | 
			
		||||
	Port                     string `mapstructure:"PORT"`
 | 
			
		||||
	APIKey                   string `mapstructure:"APIKEY"`
 | 
			
		||||
	EventName                string `mapstructure:"EVENTNAME"`
 | 
			
		||||
	CurrencySymbol           string `mapstructure:"CURRENCYSYMBOL"`
 | 
			
		||||
	CurrencyIdentifier       string `mapstructure:"CURRENCYIDENTIFIER"`
 | 
			
		||||
	CardSubtitle             string `mapstructure:"CARD_SUBTITLE"`
 | 
			
		||||
	CardBarcodeFormat        string `mapstructure:"CARD_BARCODEFORMAT"`
 | 
			
		||||
	CardBarcodePrefix        string `mapstructure:"CARD_BARCODEPREFIX"`
 | 
			
		||||
	SponsoringReceiptMinimum int    `mapstructure:"SPONSOING_RECEIPTMINIMUM"`
 | 
			
		||||
	SponsoringReceiptMinimum string `mapstructure:"SPONSORING_RECEIPTMINIMUM"`
 | 
			
		||||
	SponosringDisclaimer     string `mapstructure:"SPONSORING_DISCLAIMER"`
 | 
			
		||||
	SponsoringBarcodeFormat  string `mapstructure:"SPONSORING_BARCODEFORMAT"`
 | 
			
		||||
	SponsoringBarcodePrefix  string `mapstructure:"SPONSORING_BARCODEPREFIX"`
 | 
			
		||||
	CertificateFooter        string `mapstructure:"CERTIFICATE_FOOTER"`
 | 
			
		||||
	GotenbergBaseUrl         string `mapstructure:"GOTENBERG_BASEURL"`
 | 
			
		||||
	RedisAddr                string `mapstructure:"REDIS_ADDR"`
 | 
			
		||||
	SepaBic                  string `mapstructure:"SEPA_BIC"`
 | 
			
		||||
	SepaName                 string `mapstructure:"SEPA_NAME"`
 | 
			
		||||
	SepaIban                 string `mapstructure:"SEPA_IBAN"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,14 +14,17 @@ type Runner struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Group struct {
 | 
			
		||||
	Name string `json:"name" validate:"required"`
 | 
			
		||||
	Name        string `json:"name" validate:"required"`
 | 
			
		||||
	ParentGroup struct {
 | 
			
		||||
		Name string `json:"name" validate:"required"`
 | 
			
		||||
	} `json:"parent_group" validate:"optional"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ContractTemplateOptions struct {
 | 
			
		||||
	Runners              []Runner `json:"runners"`
 | 
			
		||||
	CurrencySymbol       string   `json:"currency_symbol"`
 | 
			
		||||
	Disclaimer           string   `json:"disclaimer"`
 | 
			
		||||
	ReceiptMinimumAmount int      `json:"receipt_minimum_amount"`
 | 
			
		||||
	ReceiptMinimumAmount string   `json:"receipt_minimum_amount"`
 | 
			
		||||
	EventName            string   `json:"event_name"`
 | 
			
		||||
	BarcodeFormat        string   `json:"barcode_format"`
 | 
			
		||||
	BarcodePrefix        string   `json:"barcode_prefix"`
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ import (
 | 
			
		||||
	"image/color"
 | 
			
		||||
	"image/draw"
 | 
			
		||||
	"image/png"
 | 
			
		||||
	"log"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
@@ -17,6 +16,7 @@ import (
 | 
			
		||||
	"github.com/boombuler/barcode/ean"
 | 
			
		||||
	"github.com/boombuler/barcode/qr"
 | 
			
		||||
	"github.com/redis/go-redis/v9"
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BarcodeService interface {
 | 
			
		||||
@@ -26,19 +26,26 @@ type BarcodeService interface {
 | 
			
		||||
 | 
			
		||||
type DefaultBarcodeService struct {
 | 
			
		||||
	RedisClient *redis.Client
 | 
			
		||||
	Logger      *zap.SugaredLogger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, width int, height int, padding int) (bytes.Buffer, error) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	logger := b.Logger.Named("GenerateBarcode")
 | 
			
		||||
 | 
			
		||||
	if !b.IsTypeSupported(format) {
 | 
			
		||||
		logger.Errorw("Unsupported barcode type", "type", format)
 | 
			
		||||
		return bytes.Buffer{}, fmt.Errorf("unsupported barcode type: %s", format)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger = logger.With("type", format, "content", content, "width", width, "height", height, "padding", padding)
 | 
			
		||||
	cacheKey := fmt.Sprintf("barcode:%s:%s:%d:%d:%d", format, content, width, height, padding)
 | 
			
		||||
 | 
			
		||||
	if b.RedisClient != nil {
 | 
			
		||||
		cachedBarcode, err := b.RedisClient.Get(ctx, fmt.Sprintf("barcode:%s:%s:%d:%d:%d", format, content, width, height, padding)).Result()
 | 
			
		||||
		logger.Debugw("Checking cache for barcode", "key", cacheKey)
 | 
			
		||||
		cachedBarcode, err := b.RedisClient.Get(ctx, cacheKey).Result()
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			log.Printf("Cache hit for barcode:%s:%s:%d:%d", format, content, width, height)
 | 
			
		||||
			logger.Infow("Barcode found in cache", "key", cacheKey)
 | 
			
		||||
			buf := bytes.Buffer{}
 | 
			
		||||
			buf.Write([]byte(cachedBarcode))
 | 
			
		||||
			return buf, nil
 | 
			
		||||
@@ -62,7 +69,11 @@ func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, w
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	case "qr":
 | 
			
		||||
		generatedCode, err = qr.Encode(content, qr.M, qr.AlphaNumeric)
 | 
			
		||||
		// Always use qr.Auto encoding to support all characters in the content
 | 
			
		||||
		encoding := qr.Auto
 | 
			
		||||
 | 
			
		||||
		// QR code generation with error correction level M and auto encoding
 | 
			
		||||
		generatedCode, err = qr.Encode(content, qr.M, encoding)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return bytes.Buffer{}, err
 | 
			
		||||
		}
 | 
			
		||||
@@ -73,6 +84,7 @@ func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, w
 | 
			
		||||
	bg := image.NewRGBA(image.Rect(0, 0, width, height))
 | 
			
		||||
	white := color.RGBA{255, 255, 255, 255}
 | 
			
		||||
	draw.Draw(bg, bg.Bounds(), &image.Uniform{white}, image.Point{}, draw.Src)
 | 
			
		||||
	logger.Debug("Created white background")
 | 
			
		||||
 | 
			
		||||
	// Calculate the new size for the barcode to fit within the padding
 | 
			
		||||
	newWidth := width - 2*padding
 | 
			
		||||
@@ -81,24 +93,32 @@ func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, w
 | 
			
		||||
	// Scale the barcode to the new size
 | 
			
		||||
	scaledCode, err := barcode.Scale(generatedCode, newWidth, newHeight)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to scale barcode", "error", err)
 | 
			
		||||
		return bytes.Buffer{}, err
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debug("Scaled barcode")
 | 
			
		||||
 | 
			
		||||
	// Draw the barcode on top of the white background with padding
 | 
			
		||||
	draw.Draw(bg, scaledCode.Bounds().Add(image.Point{padding, padding}), scaledCode, image.Point{}, draw.Over)
 | 
			
		||||
	logger.Debug("Drew barcode on background")
 | 
			
		||||
 | 
			
		||||
	var buf bytes.Buffer
 | 
			
		||||
	err = png.Encode(&buf, bg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to encode barcode to PNG", "error", err)
 | 
			
		||||
		return bytes.Buffer{}, err
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debug("Encoded barcode to PNG")
 | 
			
		||||
 | 
			
		||||
	if b.RedisClient != nil {
 | 
			
		||||
		err = b.RedisClient.Set(ctx, fmt.Sprintf("barcode:%s:%s:%d:%d", format, content, width, height), buf.String(), 10*time.Minute).Err()
 | 
			
		||||
		err = b.RedisClient.Set(ctx, cacheKey, buf.String(), 10*time.Minute).Err()
 | 
			
		||||
		logger.Debugw("Cached barcode", "key", cacheKey)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Errorw("Failed to cache barcode", "error", err)
 | 
			
		||||
			return bytes.Buffer{}, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	logger.Info("Generated barcode")
 | 
			
		||||
 | 
			
		||||
	return buf, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/oxplot/papersizes"
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Converter interface {
 | 
			
		||||
@@ -16,92 +17,115 @@ type Converter interface {
 | 
			
		||||
 | 
			
		||||
type GotenbergConverter struct {
 | 
			
		||||
	BaseUrl string
 | 
			
		||||
	Logger  *zap.SugaredLogger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *GotenbergConverter) ToPdf(html string, pageSize string, landscape bool) ([]byte, error) {
 | 
			
		||||
	logger := g.Logger.Named("ToPdf").With("page_size", pageSize, "landscape", landscape, "base_url", g.BaseUrl)
 | 
			
		||||
 | 
			
		||||
	client := &http.Client{}
 | 
			
		||||
	defer client.CloseIdleConnections()
 | 
			
		||||
	logger.Debug("Created HTTP client")
 | 
			
		||||
 | 
			
		||||
	body := &bytes.Buffer{}
 | 
			
		||||
	writer := multipart.NewWriter(body)
 | 
			
		||||
 | 
			
		||||
	part, err := writer.CreateFormFile("files", "index.html")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to create form file", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = part.Write([]byte(html))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to write to form file", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	size := papersizes.FromName(pageSize)
 | 
			
		||||
	if size == nil {
 | 
			
		||||
		logger.Errorw("Invalid page size", "size", pageSize)
 | 
			
		||||
		return nil, fmt.Errorf("invalid page size: %s", pageSize)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = writer.WriteField("paperWidth", strconv.Itoa(size.Width)+"mm")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to write paper width", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = writer.WriteField("paperHeight", strconv.Itoa(size.Height)+"mm")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to write paper height", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = writer.WriteField("landscape", strconv.FormatBool(landscape))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to write landscape", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = writer.WriteField("marginTop", "0")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to write margin top", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = writer.WriteField("marginBottom", "0")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to write margin bottom", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = writer.WriteField("marginLeft", "0")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to write margin left", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = writer.WriteField("marginRight", "0")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to write margin right", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = writer.WriteField("preferCssPageSize", "true")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to write prefer css page size", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = writer.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to close writer", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debug("Created form data")
 | 
			
		||||
 | 
			
		||||
	logger.Debug("Creating HTTP request")
 | 
			
		||||
	req, err := http.NewRequest("POST", g.BaseUrl+"/forms/chromium/convert/html", body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Set("Content-Type", writer.FormDataContentType())
 | 
			
		||||
 | 
			
		||||
	logger.Debug("Sending HTTP request")
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to send request", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debug("Received HTTP response")
 | 
			
		||||
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	data := new(bytes.Buffer)
 | 
			
		||||
	_, err = data.ReadFrom(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to read response body", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Debug("Returning PDF data")
 | 
			
		||||
	return data.Bytes(), nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,8 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Templater interface {
 | 
			
		||||
@@ -17,6 +19,7 @@ type Templater interface {
 | 
			
		||||
type DefaultTemplater struct {
 | 
			
		||||
	BarcodeService BarcodeService
 | 
			
		||||
	StaticService  StaticService
 | 
			
		||||
	Logger         *zap.SugaredLogger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func idToEan13(id string, prefix string) (string, error) {
 | 
			
		||||
@@ -32,6 +35,28 @@ func idToEan13(id string, prefix string) (string, error) {
 | 
			
		||||
	return id, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *DefaultTemplater) GenerateEPC(iban string, bic string, name string, title string, amount int, currency string) (string, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	code := fmt.Sprintf(`BCD
 | 
			
		||||
002
 | 
			
		||||
1
 | 
			
		||||
INST
 | 
			
		||||
%s
 | 
			
		||||
%s
 | 
			
		||||
%s
 | 
			
		||||
%s%.2f
 | 
			
		||||
 | 
			
		||||
%s
 | 
			
		||||
 | 
			
		||||
`, bic, name, iban, currency, float64(amount)/100, title,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	buf, err := t.BarcodeService.GenerateBarcode("qr", code, 500, 500, 0)
 | 
			
		||||
 | 
			
		||||
	return base64.StdEncoding.EncodeToString(buf.Bytes()), err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix string) (string, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
@@ -48,10 +73,13 @@ func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix st
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *DefaultTemplater) SelectSponsorImage(id int) (string, error) {
 | 
			
		||||
	logger := t.Logger.Named("SelectSponsorImage")
 | 
			
		||||
	sponsors, err := t.StaticService.ListFilesInStaticSubFolder("images/sponsors")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorw("Failed to list sponsors", "error", err)
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debugw("Selected sponsor", "sponsors", sponsors, "id", id, "selected", sponsors[id%len(sponsors)])
 | 
			
		||||
	return t.StaticService.GetImage("sponsors/" + strings.TrimSuffix(sponsors[id%len(sponsors)], ".base64")), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -82,6 +110,7 @@ func (t *DefaultTemplater) StringToTemplate(templateString string) (*template.Te
 | 
			
		||||
		"sponsorLogo": t.SelectSponsorImage,
 | 
			
		||||
		"formatUnit":  t.FormatUnit,
 | 
			
		||||
		"loadImage":   t.LoadImage,
 | 
			
		||||
		"epcCode":     t.GenerateEPC,
 | 
			
		||||
	}).Parse(templateString)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,9 @@ package services
 | 
			
		||||
import (
 | 
			
		||||
	_ "embed"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type StaticService interface {
 | 
			
		||||
@@ -14,47 +15,55 @@ type StaticService interface {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DefaultStaticService struct {
 | 
			
		||||
	Cache map[string]string
 | 
			
		||||
	Cache  map[string]string
 | 
			
		||||
	Logger *zap.SugaredLogger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DefaultStaticService) GetTemplate(locale, templateName string) (string, error) {
 | 
			
		||||
	logger := s.Logger.Named("GetTemplate").With("locale", locale, "template_name", templateName)
 | 
			
		||||
 | 
			
		||||
	if s.Cache[locale+templateName] != "" {
 | 
			
		||||
		log.Printf("returning cached template %s with locale %s", templateName, locale)
 | 
			
		||||
		logger.Debugw("Template found in cache", "key", locale+templateName)
 | 
			
		||||
		return s.Cache[locale+templateName], nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content, err := os.ReadFile(fmt.Sprintf("static/templates/%s/%s.html", templateName, locale))
 | 
			
		||||
	if content == nil || err != nil {
 | 
			
		||||
		log.Printf("error reading template %s with locale %s: %v", templateName, locale, err)
 | 
			
		||||
		logger.Errorw("Failed to read template", "error", err)
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.Cache[locale+templateName] = string(content)
 | 
			
		||||
	logger.Debugw("Saved template to cache", "key", locale+templateName)
 | 
			
		||||
 | 
			
		||||
	return string(content), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DefaultStaticService) ListFilesInStaticSubFolder(folderName string) ([]string, error) {
 | 
			
		||||
	logger := s.Logger.Named("ListFilesInStaticSubFolder").With("folder_name", folderName)
 | 
			
		||||
 | 
			
		||||
	files, err := os.ReadDir(fmt.Sprintf("static/%s", folderName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("error reading files from folder %s: %v", folderName, err)
 | 
			
		||||
		logger.Errorw("Failed to list files", "error", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	var images []string
 | 
			
		||||
	for _, file := range files {
 | 
			
		||||
		if file.IsDir() {
 | 
			
		||||
			continue
 | 
			
		||||
			logger.Debugw("Skipping directory", "file", file.Name())
 | 
			
		||||
		}
 | 
			
		||||
		images = append(images, file.Name())
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debugw("Listed files", "files", images)
 | 
			
		||||
	return images, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DefaultStaticService) GetImage(imageName string) string {
 | 
			
		||||
	logger := s.Logger.Named("GetImage").With("image_name", imageName)
 | 
			
		||||
 | 
			
		||||
	content, err := os.ReadFile("static/images/" + imageName + ".base64")
 | 
			
		||||
	if content == nil || err != nil {
 | 
			
		||||
		log.Printf("error reading image %s: %v", imageName, err)
 | 
			
		||||
		logger.Errorw("Failed to read image", "error", err)
 | 
			
		||||
		return ImageErrorBase64
 | 
			
		||||
	}
 | 
			
		||||
	return string(content)
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								static/images/sponsors/atlantis.base64
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/images/sponsors/atlantis.base64
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								static/images/sponsors/herzogspark.base64
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/images/sponsors/herzogspark.base64
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf8">
 | 
			
		||||
  <title>Sponsoring contract</title>
 | 
			
		||||
  <title>Läuferkarten</title>
 | 
			
		||||
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
 | 
			
		||||
  <style>
 | 
			
		||||
    .sheet {
 | 
			
		||||
@@ -50,8 +50,12 @@
 | 
			
		||||
            <p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {{ if ne .Runner.FirstName "" }}
 | 
			
		||||
        <p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p>
 | 
			
		||||
        <p>{{ .Runner.Group.Name }}</p>
 | 
			
		||||
        <p>{{ if ne .Runner.Group.ParentGroup.Name "" -}}{{ .Runner.Group.ParentGroup.Name }}/{{end -}}{{ .Runner.Group.Name }}</p>
 | 
			
		||||
        {{ else }}
 | 
			
		||||
        <p>Kein Läufer zugewiesen</p>
 | 
			
		||||
        {{ end}}
 | 
			
		||||
        {{ end}}
 | 
			
		||||
      </div>
 | 
			
		||||
      {{ end }}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf8">
 | 
			
		||||
  <title>Sponsoring contract</title>
 | 
			
		||||
  <title>Runner cards</title>
 | 
			
		||||
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
 | 
			
		||||
  <style>
 | 
			
		||||
    .sheet {
 | 
			
		||||
@@ -49,8 +49,12 @@
 | 
			
		||||
            <p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {{ if ne .Runner.FirstName ""}}
 | 
			
		||||
        <p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p>
 | 
			
		||||
        <p>{{ .Runner.Group.Name }}</p>
 | 
			
		||||
        <p>{{ if ne .Runner.Group.ParentGroup.Name "" -}}{{ .Runner.Group.ParentGroup.Name }}/{{end -}}{{ .Runner.Group.Name }}</p>
 | 
			
		||||
        {{ else }}
 | 
			
		||||
        <p>Blank card</p>
 | 
			
		||||
        {{ end}}
 | 
			
		||||
      </div>
 | 
			
		||||
      {{ end }}
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf8">
 | 
			
		||||
  <title>Sponsoring contract</title>
 | 
			
		||||
  <title>Läuferurkunde</title>
 | 
			
		||||
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
 | 
			
		||||
  <style>
 | 
			
		||||
    .sheet {
 | 
			
		||||
@@ -87,13 +87,30 @@
 | 
			
		||||
          <td>Gesamt</td>
 | 
			
		||||
          <td>{{ formatUnit "euro" $.Locale .TotalPerDistance }} {{ $.CurrencySymbol }}</td>
 | 
			
		||||
          <td>{{ formatUnit "euro" $.Locale .TotalDonations }} {{ $.CurrencySymbol }}</td>
 | 
			
		||||
        </tfoot>
 | 
			
		||||
      </table>
 | 
			
		||||
    </main>
 | 
			
		||||
    <footer class="certificate-footer">
 | 
			
		||||
      <p>
 | 
			
		||||
        {{ $.Footer }}
 | 
			
		||||
      </p>
 | 
			
		||||
        </table>
 | 
			
		||||
        </main>
 | 
			
		||||
        <footer class="certificate-footer">
 | 
			
		||||
            <table style="border-collapse: collapse; border: none; width: 17cm;">
 | 
			
		||||
            <thead>
 | 
			
		||||
              <tr>
 | 
			
		||||
              <th style="border: none; width: 50%; text-align: center;">Link zu deinen Rundenzeiten</th>
 | 
			
		||||
              <th style="border: none; width: 50%; text-align: center;">Spende überweisen</th>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr>
 | 
			
		||||
              <td style="border: none; text-align: center;">
 | 
			
		||||
                <img src="data:image/png;base64,{{ barcode .SelfServiceLink "qr" "" }}" style="height: 2.5cm; padding: 0.2cm">
 | 
			
		||||
              </td>
 | 
			
		||||
              <td style="border: none; text-align: center;">
 | 
			
		||||
                <img src="data:image/png;base64,{{ epcCode $.SepaConfig.IBAN $.SepaConfig.BIC $.SepaConfig.HolderName (print "Spende LfK " .ID ", "  .FirstName " " .LastName ", " .CombinedGroupName) .TotalDonations $.SepaConfig.CurrencyIdentifier}}" style="height: 2.5cm; padding: 0.2cm">
 | 
			
		||||
              </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
            <p style="width: 17cm; text-align: center;">
 | 
			
		||||
              Sponsoren überweisen ihre Beträge bitte auf unser Konto: {{ $.SepaConfig.HolderName }} | IBAN: {{ $.SepaConfig.IBAN }} | BIC: {{ $.SepaConfig.BIC }} | Vz: "Spende LfK {{.ID}}, {{ .FirstName }} {{ .LastName }}, {{.CombinedGroupName}}"
 | 
			
		||||
            </p>
 | 
			
		||||
    </footer>
 | 
			
		||||
  </article>
 | 
			
		||||
  {{ end }}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf8">
 | 
			
		||||
  <title>Sponsoring contract</title>
 | 
			
		||||
  <title>Runner certificate</title>
 | 
			
		||||
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
 | 
			
		||||
  <style>
 | 
			
		||||
    .sheet {
 | 
			
		||||
@@ -91,8 +91,26 @@
 | 
			
		||||
      </table>
 | 
			
		||||
    </main>
 | 
			
		||||
    <footer class="certificate-footer">
 | 
			
		||||
      <p>
 | 
			
		||||
        {{ $.Footer }}
 | 
			
		||||
      <table style="border-collapse: collapse; border: none; width: 17cm;">
 | 
			
		||||
        <thead>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th style="border: none; width: 50%; text-align: center;">Link to your lap times</th>
 | 
			
		||||
            <th style="border: none; width: 50%; text-align: center;">Transfer donation via SEPA</th>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr>
 | 
			
		||||
          <td style="border: none; text-align: center;">
 | 
			
		||||
            <img src="data:image/png;base64,{{ barcode .SelfServiceLink "qr" "" }}" style="height: 2.5cm; padding: 0.2cm">
 | 
			
		||||
          </td>
 | 
			
		||||
          <td style="border: none; text-align: center;">
 | 
			
		||||
            <img src="data:image/png;base64,{{ epcCode $.SepaConfig.IBAN $.SepaConfig.BIC $.SepaConfig.HolderName (print "Spende LfK " .ID ", "  .FirstName " " .LastName ", " .CombinedGroupName) .TotalDonations $.SepaConfig.CurrencyIdentifier}}" style="height: 2.5cm; padding: 0.2cm">
 | 
			
		||||
          </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      <p style="width: 17cm; text-align: center;">
 | 
			
		||||
        Donors, please transfer your donation to our account: {{ $.SepaConfig.HolderName }} | IBAN: {{ $.SepaConfig.IBAN }} | BIC: {{ $.SepaConfig.BIC }} | Ref: "Spende LfK {{.ID}}, {{ .FirstName }} {{ .LastName }}, {{.CombinedGroupName}}"
 | 
			
		||||
      </p>
 | 
			
		||||
    </footer>
 | 
			
		||||
  </article>
 | 
			
		||||
 
 | 
			
		||||
@@ -65,8 +65,8 @@
 | 
			
		||||
          <p style="font-size: x-small; display: block;">Nachname</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="column is-6">
 | 
			
		||||
          <span style="border-bottom: 1px solid; width: 100%; display: block;">{{ .Group.Name }}</span>
 | 
			
		||||
          <p style="font-size: x-small; display: block;">Team/ Klasse</p>
 | 
			
		||||
          <span style="border-bottom: 1px solid; width: 100%; display: block;"><p>{{ if ne .Group.ParentGroup.Name "" -}}{{ .Group.ParentGroup.Name }}/ {{end -}}{{ .Group.Name }}</p></span>
 | 
			
		||||
          <p style="font-size: x-small; display: block;">Team/Klasse</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <p style="margin-top: -0.5rem">mit einem Betrag von _____ {{ $.CurrencySymbol }} pro gelaufenem Kilometer zu
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,7 @@
 | 
			
		||||
          <p style="font-size: x-small; display: block;">Last Name</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="column is-6">
 | 
			
		||||
          <span style="border-bottom: 1px solid; width: 100%; display: block;">{{ .Group.Name}}</span>
 | 
			
		||||
          <span style="border-bottom: 1px solid; width: 100%; display: block;"><p>{{ if ne .Group.ParentGroup.Name "" -}}{{ .Group.ParentGroup.Name }}/ {{end -}}{{ .Group.Name }}</p></span>
 | 
			
		||||
          <p style="font-size: x-small; display: block;">Team/class</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user