LINUS ROUND 1: Delete dead code (rust/c), generic Store[T], tests, slim main.go
- Removed rust-service/, c-runtime/, kafka stubs - Generic Store[T] pattern with real tests - Slimmed main.go from 324 to ~50 lines - Added config, middleware, store, ledger, pdf tests - Frontend SPA shell with router - Binary: 15.5MB -> 12MB
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
// Package pdf provides generic document generation.
|
||||
// One function, multiple document types. Linus-style.
|
||||
package pdf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jung-kurt/gofpdf"
|
||||
)
|
||||
|
||||
// DocType represents the type of document
|
||||
type DocType string
|
||||
|
||||
const (
|
||||
InvoiceDoc DocType = "FAKTURA"
|
||||
QuoteDoc DocType = "OFFERT"
|
||||
)
|
||||
|
||||
// LineItem represents a single line on a document
|
||||
type LineItem struct {
|
||||
Description string
|
||||
Quantity float64
|
||||
Unit string
|
||||
UnitPrice float64
|
||||
Total float64
|
||||
}
|
||||
|
||||
// DocumentData contains all data needed to generate a document
|
||||
type DocumentData struct {
|
||||
DocType DocType
|
||||
DocNumber string
|
||||
DocDate time.Time
|
||||
ValidUntil *time.Time
|
||||
CustomerName string
|
||||
CustomerAddress string
|
||||
CustomerOrgNr string
|
||||
Items []LineItem
|
||||
Subtotal float64
|
||||
VATRate float64
|
||||
VATAmount float64
|
||||
Total float64
|
||||
Currency string
|
||||
CompanyName string
|
||||
CompanyAddress string
|
||||
CompanyOrgNr string
|
||||
CompanyBankgiro string
|
||||
Notes string
|
||||
}
|
||||
|
||||
// GenerateDocument creates a professional PDF document
|
||||
func GenerateDocument(data DocumentData) ([]byte, error) {
|
||||
pdf := gofpdf.New("P", "mm", "A4", "")
|
||||
pdf.AddPage()
|
||||
|
||||
// Header with document type
|
||||
pdf.SetFont("Arial", "B", 20)
|
||||
pdf.SetTextColor(201, 106, 58) // Terracotta
|
||||
pdf.Cell(0, 12, string(data.DocType))
|
||||
pdf.Ln(8)
|
||||
|
||||
// Company info
|
||||
pdf.SetFont("Arial", "B", 10)
|
||||
pdf.SetTextColor(50, 50, 50)
|
||||
pdf.Cell(0, 5, data.CompanyName)
|
||||
pdf.Ln(5)
|
||||
pdf.SetFont("Arial", "", 9)
|
||||
pdf.Cell(0, 4, data.CompanyAddress)
|
||||
pdf.Ln(4)
|
||||
if data.CompanyOrgNr != "" {
|
||||
pdf.Cell(0, 4, fmt.Sprintf("Org.nr: %s", data.CompanyOrgNr))
|
||||
pdf.Ln(4)
|
||||
}
|
||||
if data.CompanyBankgiro != "" {
|
||||
pdf.Cell(0, 4, fmt.Sprintf("Bankgiro: %s", data.CompanyBankgiro))
|
||||
pdf.Ln(4)
|
||||
}
|
||||
pdf.Ln(5)
|
||||
|
||||
// Document details box
|
||||
pdf.SetFillColor(250, 248, 245)
|
||||
pdf.Rect(130, 30, 70, 35, "F")
|
||||
pdf.SetXY(135, 33)
|
||||
pdf.SetFont("Arial", "B", 9)
|
||||
pdf.SetTextColor(201, 106, 58)
|
||||
pdf.Cell(0, 5, docInfoLabel(data.DocType))
|
||||
pdf.Ln(6)
|
||||
pdf.SetFont("Arial", "", 9)
|
||||
pdf.SetTextColor(50, 50, 50)
|
||||
pdf.SetX(135)
|
||||
pdf.Cell(0, 4, fmt.Sprintf("%s: %s", docNumberLabel(data.DocType), data.DocNumber))
|
||||
pdf.Ln(4)
|
||||
pdf.SetX(135)
|
||||
pdf.Cell(0, 4, fmt.Sprintf("Datum: %s", data.DocDate.Format("2006-01-02")))
|
||||
pdf.Ln(4)
|
||||
if data.ValidUntil != nil {
|
||||
pdf.SetX(135)
|
||||
pdf.Cell(0, 4, fmt.Sprintf("Giltig till: %s", data.ValidUntil.Format("2006-01-02")))
|
||||
pdf.Ln(4)
|
||||
}
|
||||
pdf.SetX(135)
|
||||
pdf.Cell(0, 4, fmt.Sprintf("Valuta: %s", data.Currency))
|
||||
pdf.Ln(4)
|
||||
|
||||
// Customer info
|
||||
pdf.SetXY(10, 75)
|
||||
pdf.SetFont("Arial", "B", 10)
|
||||
pdf.SetTextColor(201, 106, 58)
|
||||
pdf.Cell(0, 5, "KUND")
|
||||
pdf.Ln(6)
|
||||
pdf.SetFont("Arial", "B", 10)
|
||||
pdf.SetTextColor(50, 50, 50)
|
||||
pdf.Cell(0, 5, data.CustomerName)
|
||||
pdf.Ln(5)
|
||||
pdf.SetFont("Arial", "", 9)
|
||||
pdf.Cell(0, 4, data.CustomerAddress)
|
||||
pdf.Ln(4)
|
||||
if data.CustomerOrgNr != "" {
|
||||
pdf.Cell(0, 4, fmt.Sprintf("Org.nr: %s", data.CustomerOrgNr))
|
||||
pdf.Ln(4)
|
||||
}
|
||||
pdf.Ln(10)
|
||||
|
||||
// Items table header
|
||||
pdf.SetFillColor(201, 106, 58)
|
||||
pdf.SetTextColor(255, 255, 255)
|
||||
pdf.SetFont("Arial", "B", 9)
|
||||
pdf.Cell(80, 8, "Beskrivning")
|
||||
pdf.Cell(25, 8, "Antal")
|
||||
pdf.Cell(25, 8, "Enhet")
|
||||
pdf.Cell(30, 8, "Pris")
|
||||
pdf.Cell(30, 8, "Belopp")
|
||||
pdf.Ln(8)
|
||||
|
||||
// Items
|
||||
pdf.SetTextColor(50, 50, 50)
|
||||
pdf.SetFont("Arial", "", 9)
|
||||
for i, item := range data.Items {
|
||||
if i%2 == 0 {
|
||||
pdf.SetFillColor(250, 248, 245)
|
||||
pdf.Rect(10, pdf.GetY(), 190, 6, "F")
|
||||
}
|
||||
pdf.Cell(80, 6, item.Description)
|
||||
pdf.Cell(25, 6, fmt.Sprintf("%.2f", item.Quantity))
|
||||
pdf.Cell(25, 6, item.Unit)
|
||||
pdf.Cell(30, 6, fmt.Sprintf("%.2f", item.UnitPrice))
|
||||
pdf.Cell(30, 6, fmt.Sprintf("%.2f", item.Total))
|
||||
pdf.Ln(6)
|
||||
}
|
||||
|
||||
pdf.Ln(5)
|
||||
|
||||
// Totals
|
||||
pdf.SetX(120)
|
||||
pdf.SetFont("Arial", "", 9)
|
||||
pdf.Cell(40, 5, "Delsumma:")
|
||||
pdf.Cell(30, 5, fmt.Sprintf("%.2f %s", data.Subtotal, data.Currency))
|
||||
pdf.Ln(5)
|
||||
pdf.SetX(120)
|
||||
pdf.Cell(40, 5, fmt.Sprintf("Moms (%.0f%%):", data.VATRate*100))
|
||||
pdf.Cell(30, 5, fmt.Sprintf("%.2f %s", data.VATAmount, data.Currency))
|
||||
pdf.Ln(5)
|
||||
pdf.SetX(120)
|
||||
pdf.SetFont("Arial", "B", 11)
|
||||
pdf.SetTextColor(201, 106, 58)
|
||||
totalLabel := "ATT BETALA:"
|
||||
if data.DocType == QuoteDoc {
|
||||
totalLabel = "TOTALT:"
|
||||
}
|
||||
pdf.Cell(40, 7, totalLabel)
|
||||
pdf.Cell(30, 7, fmt.Sprintf("%.2f %s", data.Total, data.Currency))
|
||||
pdf.Ln(10)
|
||||
|
||||
// Notes
|
||||
if data.Notes != "" {
|
||||
pdf.SetFont("Arial", "I", 8)
|
||||
pdf.SetTextColor(100, 100, 100)
|
||||
pdf.MultiCell(0, 4, data.Notes, "", "", false)
|
||||
}
|
||||
|
||||
// Footer
|
||||
pdf.SetY(-20)
|
||||
pdf.SetFont("Arial", "", 8)
|
||||
pdf.SetTextColor(150, 150, 150)
|
||||
pdf.Cell(0, 4, fmt.Sprintf("%s | %s %s | Sida %d", data.CompanyName, data.DocType, data.DocNumber, pdf.PageNo()))
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pdf.Output(&buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func docInfoLabel(dt DocType) string {
|
||||
if dt == QuoteDoc {
|
||||
return "OFFERTINFORMATION"
|
||||
}
|
||||
return "FAKTURAINFORMATION"
|
||||
}
|
||||
|
||||
func docNumberLabel(dt DocType) string {
|
||||
if dt == QuoteDoc {
|
||||
return "Offertnr"
|
||||
}
|
||||
return "Fakturanr"
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package pdf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateDocument_Invoice(t *testing.T) {
|
||||
data := DocumentData{
|
||||
DocType: InvoiceDoc,
|
||||
DocNumber: "INV-001",
|
||||
DocDate: time.Now(),
|
||||
CustomerName: "Test AB",
|
||||
CustomerAddress: "Testgatan 1, Stockholm",
|
||||
Items: []LineItem{
|
||||
{Description: "Konsulting", Quantity: 10, Unit: "tim", UnitPrice: 1000, Total: 10000},
|
||||
},
|
||||
Subtotal: 10000,
|
||||
VATRate: 0.25,
|
||||
VATAmount: 2500,
|
||||
Total: 12500,
|
||||
Currency: "USD",
|
||||
CompanyName: "Landvex Inc",
|
||||
CompanyAddress: "Houston, TX",
|
||||
Notes: "Betalningsvillkor: 30 dagar",
|
||||
}
|
||||
|
||||
pdfBytes, err := GenerateDocument(data)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, pdfBytes)
|
||||
assert.Greater(t, len(pdfBytes), 1000)
|
||||
|
||||
// PDF magic number
|
||||
assert.Equal(t, "%PDF", string(pdfBytes[:4]))
|
||||
}
|
||||
|
||||
func TestGenerateDocument_Quote(t *testing.T) {
|
||||
validUntil := time.Now().AddDate(0, 1, 0)
|
||||
data := DocumentData{
|
||||
DocType: QuoteDoc,
|
||||
DocNumber: "Q-001",
|
||||
DocDate: time.Now(),
|
||||
ValidUntil: &validUntil,
|
||||
CustomerName: "Test AB",
|
||||
CustomerAddress: "Testgatan 1",
|
||||
Items: []LineItem{
|
||||
{Description: "Produkt A", Quantity: 5, Unit: "st", UnitPrice: 500, Total: 2500},
|
||||
},
|
||||
Subtotal: 2500,
|
||||
VATRate: 0.25,
|
||||
VATAmount: 625,
|
||||
Total: 3125,
|
||||
Currency: "USD",
|
||||
CompanyName: "Landvex Inc",
|
||||
CompanyAddress: "Houston, TX",
|
||||
}
|
||||
|
||||
pdfBytes, err := GenerateDocument(data)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, pdfBytes)
|
||||
assert.Equal(t, "%PDF", string(pdfBytes[:4]))
|
||||
}
|
||||
|
||||
func TestDocInfoLabel(t *testing.T) {
|
||||
assert.Equal(t, "FAKTURAINFORMATION", docInfoLabel(InvoiceDoc))
|
||||
assert.Equal(t, "OFFERTINFORMATION", docInfoLabel(QuoteDoc))
|
||||
}
|
||||
|
||||
func TestDocNumberLabel(t *testing.T) {
|
||||
assert.Equal(t, "Fakturanr", docNumberLabel(InvoiceDoc))
|
||||
assert.Equal(t, "Offertnr", docNumberLabel(QuoteDoc))
|
||||
}
|
||||
Reference in New Issue
Block a user