model

package
v0.0.0-...-4e85855 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 5, 2026 License: AGPL-3.0 Imports: 30 Imported by: 0

Documentation

Overview

model/api_token_service.go

model/invoice_service.go

file: src/go/model/models_tags.go

user_repo.go (Model snippet)

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidPassword     = fmt.Errorf("invalid password")
	ErrTokenExpired        = fmt.Errorf("token expired")
	ErrTokenInvalid        = fmt.Errorf("token invalid")
	ErrSignupTokenUsed     = fmt.Errorf("signup token already used")
	ErrSignupTokenNotFound = fmt.Errorf("signup token not found")
	ErrTokenNotFound       = fmt.Errorf("token not found")
	ErrTokenDisabled       = fmt.Errorf("token disabled")
	ErrUnauthorized        = fmt.Errorf("unauthorized")
)
View Source
var ErrNoSettingsRow = errors.New("no settings row found")

ErrNoSettingsRow is returned when no settings row exists in the database.

View Source
var ErrNotAllowed = fmt.Errorf("not allowed")

Functions

func BuildCustomerListURL

func BuildCustomerListURL(basePath string, q string, tags []string, modeAND bool, page, pageSize int) string

Helper for building a canonical pagination URL (optional)

func JoinTags

func JoinTags(a []string) string

JoinTags joins a slice of tag strings into a single comma-separated value, trimming extra spaces.

func NormalizeEmail

func NormalizeEmail(s string) string

NormalizeEmail lowercases and trims the email string

func RunMaintenance

func RunMaintenance(ctx context.Context, s *Store) error

RunMaintenance executes housekeeping tasks. Make sure tasks are idempotent and safe to run multiple times.

func SplitTags

func SplitTags(s string) []string

SplitTags splits a comma-separated string into a cleaned slice of tags, trimming whitespace and skipping empty entries.

func WriteSettingsXML

func WriteSettingsXML(outFilename string, s LetterheadSettings) ([]byte, error)

WriteSettingsXML writes the given LetterheadSettings structure as formatted XML to the specified output file (outFilename) and also returns the XML content as []byte. The file will be created or overwritten if it already exists.

Types

type APIToken

type APIToken struct {
	gorm.Model
	OwnerID     uint   `gorm:"index;not null"`               // Tenant/account the token belongs to
	UserID      *uint  `gorm:"index"`                        // Optional: user the token is associated with (nil for system tokens)
	TokenPrefix string `gorm:"size:16;index;not null"`       // First N chars of the token (for quick lookup without storing the token)
	TokenHash   string `gorm:"size:64;uniqueIndex;not null"` // Hex-encoded SHA-256(salt || token)
	Salt        string `gorm:"size:64;not null"`             // Hex-encoded per-token salt

	Name       string     `gorm:"size:100"` // Human-readable label, e.g. "CI build token"
	Scope      string     `gorm:"size:200"` // Application-defined scope, e.g. "read:contacts write:notes"
	ExpiresAt  *time.Time // Optional absolute expiry
	LastUsedAt *time.Time // Updated on successful validation (best-effort)
	Disabled   bool       `gorm:"not null;default:false"` // Soft revocation flag
}

APIToken is the persisted representation of an API token. Only a salted hash of the plaintext token is stored; the plaintext is returned exactly once at creation time (see CreateAPIToken).

func (APIToken) TableName

func (APIToken) TableName() string

TableName sets the underlying table name.

type ActivityHead

type ActivityHead struct {
	ItemType   string      `gorm:"column:item_type"` // "company" | "invoice" | "note"
	ItemID     uint        `gorm:"column:item_id"`
	CreatedAt  time.Time   `gorm:"column:created_at"`
	CompanyID  *uint       `gorm:"column:company_id"`  // Only for invoices
	ParentType *ParentType `gorm:"column:parent_type"` // Only for notes
	ParentID   *uint       `gorm:"column:parent_id"`   // Only for notes
}

ActivityHead represents a normalized, cross-entity activity item. It unifies companies, invoices, and notes into a single chronological feed.

type ActivityHydration

type ActivityHydration struct {
	Heads     []ActivityHead
	Companies map[uint]Company
	People    map[uint]Person
	Invoices  map[uint]Invoice
	Notes     map[uint]Note
}

ActivityHydration aggregates feed headers with preloaded entity data. This allows building a unified, fully-hydrated activity stream without N+1 queries.

type Company

type Company struct {
	gorm.Model
	Address1               string          `gorm:"column:address1"`
	Address2               string          `gorm:"column:address2"`
	Background             string          `gorm:"column:background"` // Free-form internal notes about the company
	ContactInvoice         string          `gorm:"column:contact_invoice"`
	DefaultTaxRate         decimal.Decimal `gorm:"column:default_tax_rate;type:decimal(20,8);"` // Monetary precision
	InvoiceCurrency        string          `gorm:"column:invoice_currency"`
	InvoiceExemptionReason string          `gorm:"column:invoice_exemption_reason"`
	InvoiceFooter          string          `gorm:"column:invoice_footer"`
	InvoiceOpening         string          `gorm:"column:invoice_opening"`
	Invoices               []Invoice       `gorm:"foreignKey:CompanyID"`
	InvoiceTaxType         string          `gorm:"column:invoice_tax_type"`
	CustomerNumber         string          `gorm:"column:customer_number"`
	Country                string          `gorm:"column:country"`
	Name                   string          `gorm:"column:name"`
	City                   string          `gorm:"column:city"`
	OwnerID                uint            `gorm:"column:owner_id"` // Tenant/account scope
	ContactInfos           []ContactInfo   `gorm:"polymorphic:Parent;polymorphicValue:company"`
	Contacts               []*Person       `gorm:"-"` // Computed/loaded separately; ignored by GORM
	Zip                    string          `gorm:"column:zip"`
	InvoiceEmail           string          `gorm:"column:invoice_email"`
	SupplierNumber         string          `gorm:"column:supplier_number"`
	VATID                  string          `gorm:"column:vat_id"` // VAT identification number
	Notes                  []Note          `gorm:"polymorphic:Parent;polymorphicValue:company;constraint:OnDelete:CASCADE;"`
}

Company is a legal entity (organization). It is owner-scoped (OwnerID) and may have invoices, contact infos, notes, and people (contacts) associated with it.

type CompanyListFilters

type CompanyListFilters struct {
	Query   string   // optional free text
	Tags    []string // display names from UI (we normalize internally)
	ModeAND bool     // true: entity must have ALL tags; false: ANY of tags
	Limit   int
	Offset  int
}

CompanyListFilters is the input for the company search.

type CompanyListResult

type CompanyListResult struct {
	Companies []Company
	Total     int64
}

CompanyListResult bundles page results.

type Config

type Config struct {
	Basedir                  string
	CookieSecret             string
	MailAPIKey               string
	MailSecret               string
	Mode                     string
	UseInvitationCodes       bool
	Port                     int
	PublishingServerAddress  string
	PublishingServerUsername string
	RegistrationAllowed      bool
	Servers                  map[string]server
	SP                       string
	XMLDir                   string
}

Config holds the application configuration, it is read from config.toml

type ContactInfo

type ContactInfo struct {
	ID        uint `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`

	OwnerID    uint       `gorm:"index"`                            // Tenant or account owner
	ParentID   uint       `gorm:"index:idx_contact_parent"`         // Entity ID (company/person)
	ParentType ParentType `gorm:"size:50;index:idx_contact_parent"` // "company" | "person"

	Type  string `gorm:"size:30;index"` // Kind of contact info
	Label string `gorm:"size:100"`      // e.g. “Office”, “HQ”, “Support”
	Value string `gorm:"size:300"`      // Actual data (phone number, email, URL, etc.)
}

ContactInfo represents a single communication channel for a parent entity. It is polymorphic, meaning it can belong to either a Company or a Person.

Examples:

Type:  "phone", "fax", "email", "website", "linkedin", "twitter", "github", "other"
Label: "Office", "Support", "Main Line"
Value: "+49 89 1234567", "info@example.com", "example.com"

The combination (OwnerID, ParentType, ParentID) identifies the entity the contact belongs to. Records are soft-deletable via GORM's DeletedAt.

func (ContactInfo) Href

func (c ContactInfo) Href() string

Href returns a URI-ready representation of the contact info's value. It prepends a suitable scheme (e.g. tel:, mailto:, https://) depending on the Type.

Examples:

Type=phone,  Value="012345"   → "tel:012345"
Type=email,  Value="a@b.com"  → "mailto:a@b.com"
Type=website, Value="foo.com" → "https://foo.com"

func (ContactInfo) OpensInNewTab

func (c ContactInfo) OpensInNewTab() bool

OpensInNewTab indicates whether the contact info link should open in a new tab/window.

func (ContactInfo) SafeHref

func (c ContactInfo) SafeHref() template.URL

SafeHref returns a template.URL version of the Href() output for safe embedding in HTML templates.

type DepartPersonResult

type DepartPersonResult struct {
	Person *Person
	Note   *Note
}

DepartPersonResult contains the result of a DepartPerson operation

type EntityType

type EntityType string

EntityType defines the type of entity that was recently viewed

const (
	// EntityCompany represents a company entity
	EntityCompany EntityType = "company"
	// EntityPerson represents a person entity
	EntityPerson EntityType = "person"
)

type FieldKind

type FieldKind string
const (
	// Fixed set for the editor
	FieldSender      FieldKind = "addressee"    // "Recipient"
	FieldInvoiceInfo FieldKind = "invoice_info" // "Rechnungsangaben"
	FieldPositions   FieldKind = "main_area"    // table area (may have page 2 coords)
)

type FontsBlock

type FontsBlock struct {
	Normal string `xml:"normal,omitempty"`
	Bold   string `xml:"bold,omitempty"`
	Italic string `xml:"italic,omitempty"`
}

type Invitation

type Invitation struct {
	ID        uint   `gorm:"primaryKey"`
	Token     string `gorm:"uniqueIndex"`
	Email     string
	ExpiresAt *time.Time
	CreatedAt time.Time
}

type Invoice

type Invoice struct {
	gorm.Model
	CompanyID        uint
	Company          Company `gorm:"foreignKey:CompanyID"`
	ContactInvoice   string
	Counter          uint
	Currency         string
	Date             time.Time
	DueDate          time.Time
	ExemptionReason  string
	Footer           string
	GrossTotal       decimal.Decimal
	InvoicePositions []InvoicePosition
	NetTotal         decimal.Decimal
	Number           string
	OccurrenceDate   time.Time
	Opening          string // Text before invoice
	OrderNumber      string
	BuyerReference   string
	OwnerID          uint
	SupplierNumber   string
	TaxAmounts       []TaxAmount `gorm:"-"`
	TaxNumber        string
	TaxType          string
	Status           InvoiceStatus `gorm:"type:text;not null;default:draft;check:status IN ('draft','issued','paid','voided');index;index:idx_owner_status"`
	IssuedAt         *time.Time    // set when status -> issued
	PaidAt           *time.Time    // set when status -> paid
	VoidedAt         *time.Time    // set when status -> voided

	TemplateID *uint
	Template   *LetterheadTemplate `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

func (*Invoice) RecomputeTotals

func (i *Invoice) RecomputeTotals()

RecomputeTotals sets NetTotal, GrossTotal and TaxAmounts based on the positions.

type InvoiceListQuery

type InvoiceListQuery struct {
	Status    string // Optional: filter by status (application-defined values, e.g., "open", "paid")
	CompanyID uint   // Optional: restrict to a single company
	Limit     int    // Page size (1–200); defaults to 50 when out of range
	Cursor    string // Simple offset cursor encoded as a string: "0", "50", ...
	Sort      string // Sort mode: "date_desc" (default), "date_asc", "created_desc"
}

InvoiceListQuery captures filter, paging, and sorting options for listing invoices.

type InvoicePosition

type InvoicePosition struct {
	ID         uint `gorm:"primarykey"`
	CreatedAt  time.Time
	OwnerID    uint
	InvoiceID  uint
	Position   int
	UnitCode   string
	Text       string
	Quantity   decimal.Decimal `sql:"type:decimal(20,8);"`
	TaxRate    decimal.Decimal `sql:"type:decimal(20,8);"`
	NetPrice   decimal.Decimal `sql:"type:decimal(20,8);"`
	GrossPrice decimal.Decimal `sql:"type:decimal(20,8);"`
	LineTotal  decimal.Decimal `sql:"type:decimal(20,8);"`
}

InvoicePosition contains one line in the invoice

func (InvoicePosition) TableName

func (InvoicePosition) TableName() string

type InvoiceProblem

type InvoiceProblem struct {
	Level   string // "error", "warning", "info"
	Message string
}

type InvoiceStatus

type InvoiceStatus string
const (
	InvoiceStatusDraft  InvoiceStatus = "draft"
	InvoiceStatusIssued InvoiceStatus = "issued"
	InvoiceStatusPaid   InvoiceStatus = "paid"
	InvoiceStatusVoided InvoiceStatus = "voided"
)

func (InvoiceStatus) IsFinal

func (s InvoiceStatus) IsFinal() bool

type LetterheadBlock

type LetterheadBlock struct {
	Name         string         `xml:"name"`
	PageWidthCm  float64        `xml:"pageWidthCm"`
	PageHeightCm float64        `xml:"pageHeightCm"`
	Regions      []RegionExport `xml:"regions>region"`
	PDFPath      string         `xml:"pdfPath,omitempty"`
	Fonts        FontsBlock     `xml:"fonts"`
}

LetterheadBlock captures template meta and its regions.

type LetterheadSettings

type LetterheadSettings struct {
	XMLName       xml.Name        `xml:"BillingcatSettings"`
	XMLNS         string          `xml:"xmlns,attr"`
	Version       string          `xml:"version,attr"` // schema version
	GeneratedAt   time.Time       `xml:"generatedAt"`  // UTC timestamp
	InvoiceID     uint            `xml:"invoiceId"`
	InvoiceNumber string          `xml:"invoiceNumber"`
	Units         string          `xml:"units"` // "cm"
	Letterhead    LetterheadBlock `xml:"letterhead"`
}

Settings is the root node written to settings.xml

type LetterheadTemplate

type LetterheadTemplate struct {
	gorm.Model
	OwnerID         uint    `gorm:"index"`
	Name            string  `gorm:"size:200"`
	PageWidthCm     float64 // e.g., 21.0 (A4)
	PageHeightCm    float64 // e.g., 29.7 (A4)
	PDFPath         string  // server path to original PDF (optional)
	PreviewPage1URL string  // public URL to PNG page 1
	PreviewPage2URL string  // public URL to PNG page 2 (optional)
	// Important: explicit foreignKey mapping so GORM understands TemplateID below.
	Regions []PlacedRegion `gorm:"foreignKey:TemplateID;references:ID;constraint:OnDelete:CASCADE"`

	FontNormal string `gorm:"size:255"` // e.g. "Inter-Regular.ttf"
	FontBold   string `gorm:"size:255"` // e.g. "Inter-Bold.ttf"
	FontItalic string `gorm:"size:255"` // e.g. "Inter-Italic.ttf"
}

LetterheadTemplate represents a letterhead (1–2 pages) with optional predefined regions.

type Note

type Note struct {
	gorm.Model
	OwnerID    uint       `json:"owner_id"    form:"owner_id"`                 // Set server-side: tenant/owner scope
	AuthorID   uint       `json:"author_id"   form:"-"           gorm:"index"` // Set server-side: creating user
	ParentID   uint       `json:"parent_id"   form:"parent_id"`                // ID of the parent record
	ParentType ParentType `json:"parent_type" form:"parent_type"`              // "person" | "company"
	Title      string     `json:"title"       form:"title"`                    // Optional headline
	Body       string     `json:"body"        form:"body"`                     // Main text content
	Tags       string     `json:"tags"        form:"tags"`                     // Comma-separated tags (stored as CSV)
	EditedAt   time.Time  `json:"edited_at"   form:"edited_at"`                // Usually managed server-side
}

Note represents a user-authored text entry attached to a parent entity (either a person or a company). Notes are owner-scoped and can be tagged.

ParentType determines the kind of parent ("people" or "companies"). The combination (OwnerID, ParentType, ParentID) defines the attachment target.

Notes are lightweight, versionless records. EditedAt is automatically updated on save to reflect the last modification time.

func (*Note) BeforeSave

func (n *Note) BeforeSave(tx *gorm.DB) error

BeforeSave GORM hook — automatically updates EditedAt timestamp whenever the record is saved.

type NoteFilters

type NoteFilters struct {
	Search     string // Optional: search query (matches title/body/tags)
	Limit      int    // Page size (defaults to 50; capped at 200)
	Offset     int    // Offset for pagination
	ParentType string // Optional: filter by parent type
	ParentID   uint   // Optional: filter by parent ID
}

NoteFilters provides filtering and paging parameters for listing notes.

type ParentType

type ParentType string

ParentType defines the type of parent entity for certain records (notes, contacts)

const (
	// ParentTypeCompany is a customer.
	ParentTypeCompany ParentType = "company"
	// ParentTypePerson is a contact person.
	ParentTypePerson ParentType = "person"
)

func (ParentType) IsValid

func (p ParentType) IsValid() bool

IsValid checks if the ParentType is valid (= either company or person)

func (ParentType) String

func (p ParentType) String() string

type Person

type Person struct {
	gorm.Model
	OwnerID   uint   `gorm:"column:owner_id"` // Owning account (tenant) – used for scoping/authorization
	Name      string `gorm:"column:name"`
	Position  string `gorm:"column:position"` // Job title or role at the company
	EMail     string `gorm:"column:e_mail"`
	CompanyID int    `gorm:"column:company_id"`
	Company   Company
	// DepartedAt marks when the person left the company (nil = still active)
	DepartedAt *time.Time `gorm:"column:departed_at"`
	// Polymorphic association: ContactInfos belong to various parent types (here: Person).
	ContactInfos []ContactInfo `gorm:"polymorphic:Parent;polymorphicValue:person"`
	// Notes are polymorphic and cascade on delete; removing a Person deletes its Notes.
	Notes []Note `gorm:"polymorphic:Parent;polymorphicValue:person;constraint:OnDelete:CASCADE;"`
}

Person represents a natural person (human contact). It is owned by an account (OwnerID) and may be linked to a Company. Related ContactInfos and Notes are modeled via polymorphic associations.

func (*Person) HasDeparted

func (p *Person) HasDeparted() bool

HasDeparted returns true if the person has left the company

type PlacedRegion

type PlacedRegion struct {
	ID        uint      `gorm:"primaryKey" json:"id"`
	CreatedAt time.Time `json:"-"`
	UpdatedAt time.Time `json:"-"`

	TemplateID uint      `gorm:"index:idx_regions_tpl_owner;uniqueIndex:uniq_tpl_owner_kind" json:"template_id"` // FK -> LetterheadTemplate.ID
	OwnerID    uint      `gorm:"index:idx_regions_tpl_owner;uniqueIndex:uniq_tpl_owner_kind" json:"owner_id"`
	Kind       FieldKind `gorm:"type:text;uniqueIndex:uniq_tpl_owner_kind" json:"kind"`

	// Primary rectangle (typically page 1)
	Page     int     `json:"page"` // kept for compatibility; typically 1
	XCm      float64 `json:"xCm"`
	YCm      float64 `json:"yCm"`
	WidthCm  float64 `json:"widthCm"`
	HeightCm float64 `json:"heightCm"`

	// Text/layout options
	HAlign      string  `gorm:"size:10" json:"hAlign"`   // left|center|right
	VAlign      string  `gorm:"size:10" json:"vAlign"`   // top|middle|bottom (optional)
	FontName    string  `gorm:"size:50" json:"fontName"` // e.g., Helvetica
	FontSizePt  float64 `json:"fontSizePt"`
	LineSpacing float64 `json:"lineSpacing"`

	// Optional second-page rectangle for kind == "main_area"
	HasPage2  bool    `gorm:"not null;default:false" json:"hasPage2"`
	X2Cm      float64 `json:"x2Cm"`
	Y2Cm      float64 `json:"y2Cm"`
	Width2Cm  float64 `json:"width2Cm"`
	Height2Cm float64 `json:"height2Cm"`
}

PlacedRegion stores a draggable/resizable region (positions in cm). For kind == "main_area", the optional second-page rectangle is controlled via HasPage2 + *2 fields.

func (PlacedRegion) TableName

func (PlacedRegion) TableName() string

type RecentItem

type RecentItem struct {
	EntityType EntityType
	EntityID   uint
	ViewedAt   time.Time
	Name       string // Firmenname oder Personenname
}

RecentItem represents a recently viewed item with its details

type RecentView

type RecentView struct {
	UserID     uint       `gorm:"not null;index:idx_user_view,priority:1"`
	EntityType EntityType `gorm:"type:text;not null;index:idx_user_view,priority:2"`
	EntityID   uint       `gorm:"not null;index:idx_user_view,priority:3"`
	ViewedAt   time.Time  `gorm:"not null;index:idx_user_viewed_at,priority:2"`
}

RecentView tracks recently viewed entities by users

func (RecentView) TableName

func (RecentView) TableName() string

TableName sets the table name for RecentView

type RegionExport

type RegionExport struct {
	Kind        string  `xml:"kind,attr"` // e.g. "addressee","invoice","main_area"
	Page        int     `xml:"page,attr"` // primary rect page, 1-based
	XCm         float64 `xml:"xCm"`
	YCm         float64 `xml:"yCm"`
	WidthCm     float64 `xml:"widthCm"`
	HeightCm    float64 `xml:"heightCm"`
	HAlign      string  `xml:"hAlign,omitempty"`
	VAlign      string  `xml:"vAlign,omitempty"`
	FontName    string  `xml:"fontName,omitempty"`
	FontSizePt  float64 `xml:"fontSizePt,omitempty"`
	LineSpacing float64 `xml:"lineSpacing,omitempty"`

	// Optional second-page rectangle for "main_area" regions
	HasPage2  bool    `xml:"hasPage2,omitempty"`
	X2Cm      float64 `xml:"x2Cm,omitempty"`
	Y2Cm      float64 `xml:"y2Cm,omitempty"`
	Width2Cm  float64 `xml:"width2Cm,omitempty"`
	Height2Cm float64 `xml:"height2Cm,omitempty"`
}

RegionExport represents one placed region; all measurements in cm.

type Settings

type Settings struct {
	gorm.Model
	OwnerID               uint   `gorm:"uniqueIndex;column:owner_id"` // One row per owner/tenant
	CompanyName           string `gorm:"column:company_name"`
	InvoiceContact        string `gorm:"column:invoice_contact"`
	InvoiceEMail          string `gorm:"column:invoice_email"` // stored as invoice_email (not invoice_e_mail)
	ZIP                   string `gorm:"column:zip"`
	Address1              string `gorm:"column:address1"`
	Address2              string `gorm:"column:address2"`
	City                  string `gorm:"column:city"`
	CountryCode           string `gorm:"column:country_code"` // ISO 3166-1 alpha-2 recommended
	VATID                 string `gorm:"column:vat_id"`
	TAXNumber             string `gorm:"column:tax_number"`
	InvoiceNumberTemplate string `gorm:"column:invoice_number_template"` // e.g. "INV-{YYYY}-{NNNN}"
	UseLocalCounter       bool   `gorm:"column:use_local_counter"`       // if true, number increments per owner locally
	BankIBAN              string `gorm:"column:bank_iban"`
	BankName              string `gorm:"column:bank_name"`
	BankBIC               string `gorm:"column:bank_bic"`
	CustomerNumberPrefix  string `gorm:"column:customer_number_prefix"`  // e.g. "K-"
	CustomerNumberWidth   int    `gorm:"column:customer_number_width"`   // e.g. 5 -> K-00001
	CustomerNumberCounter int64  `gorm:"column:customer_number_counter"` // current counter (e.g. 1000)
}

Settings holds tenant-scoped account data such as address, invoicing details, and bank information. We keep the embedded gorm.Model (ID + timestamps) but enforce a UNIQUE owner_id so there is at most one settings row per owner.

type SignupToken

type SignupToken struct {
	ID        uint `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`

	Email      string    `gorm:"index;not null"`       // lowercase
	TokenHash  []byte    `gorm:"not null;uniqueIndex"` // sha256(token)
	ExpiresAt  time.Time `gorm:"not null"`
	ConsumedAt sql.NullTime

	// Optionally store password hash already at signup
	PasswordHash string `gorm:"not null"`
}

===== Pending Signup (separate table) ===== Holds pending signups until the email is confirmed. Optionally stores a password hash during signup (or ask again after verification).

func (*SignupToken) BeforeSave

func (t *SignupToken) BeforeSave(tx *gorm.DB) error

Normalize email before saving

type Store

type Store struct {
	Config *Config
	// contains filtered or unexported fields
}

Store wraps the GORM database connection and holds the configuration

func InitDatabase

func InitDatabase(_ *Config) (*Store, error)

InitDatabase for SQLite (pure Go)

func NewStoreFromDB

func NewStoreFromDB(db *gorm.DB, cfg *Config) *Store

NewStoreFromDB creates a Store from an existing GORM database connection. Useful for testing with in-memory databases.

func (*Store) AddTagsToCompanyByName

func (s *Store) AddTagsToCompanyByName(companyID, ownerID uint, names []string) error

Public helpers to add/replace tags by names (transactional).

func (*Store) AddTagsToPersonByName

func (s *Store) AddTagsToPersonByName(personID, ownerID uint, names []string) error

func (*Store) AuthenticateUser

func (s *Store) AuthenticateUser(email, password string) (*User, error)

func (*Store) AutoMigrateTokens

func (s *Store) AutoMigrateTokens() error

AutoMigrateTokens applies database schema migrations for the APIToken model. Should be called during setup or version upgrades.

func (*Store) CheckCustomerNumber

func (s *Store) CheckCustomerNumber(ctx context.Context, num string, excludeID uint) (ok bool, message string, err error)

CheckCustomerNumber validates whether a customer number is valid and available.

It enforces format rules from settings (prefix and numeric width) and checks uniqueness. Returns:

ok=true  -> number is syntactically valid and available (or belongs to excludeID)
ok=false -> invalid or taken; message gives human-readable reason

func (*Store) CheckPassword

func (s *Store) CheckPassword(u *User, password string) bool

func (*Store) ClearPasswordResetToken

func (s *Store) ClearPasswordResetToken(u *User) error

func (*Store) CompaniesByIDs

func (s *Store) CompaniesByIDs(ownerID any, ids []uint) (map[uint]Company, error)

func (*Store) CompanyNamesByIDs

func (s *Store) CompanyNamesByIDs(ownerID uint, ids []uint) (map[uint]string, error)

CompanyNamesByIDs returns a map of company ID → company name for a given set of IDs. Efficiently implemented via a selective scan on the "companies" table.

func (*Store) ConsumeSignupToken

func (s *Store) ConsumeSignupToken(tokenPlain string) (*User, error)

ConsumeSignupToken: validates the token and creates the user afterwards (if not existing)

func (*Store) CreateAPIToken

func (s *Store) CreateAPIToken(ownerID uint, userID *uint, name, scope string, expiresAt *time.Time) (plain string, rec *APIToken, err error)

CreateAPIToken creates a new API token record and returns its plaintext token **once**. The plaintext token is never stored — only a salted hash and prefix are persisted.

Parameters:

  • ownerID: The tenant or account that owns the token.
  • userID: Optional pointer to the user associated with this token (nil for system tokens).
  • name: A human-readable label (e.g. “CI build token”).
  • scope: Application-defined permission scope (e.g. “read:contacts”).
  • expiresAt: Optional expiration timestamp.

Returns:

  • plain: The full plaintext token (only returned once, store it securely).
  • rec: The database record containing metadata and the hashed token.
  • err: Any error that occurred during generation or persistence.

Security: The plaintext token is composed of a random prefix and salt; its hash is computed via SHA-256. The prefix allows efficient lookup without storing the full token.

func (*Store) CreateInvitation

func (s *Store) CreateInvitation(ctx context.Context, inv *Invitation) error

CreateInvitation inserts a new invitation into the database.

func (*Store) CreateNote

func (s *Store) CreateNote(n *Note) error

CreateNote inserts a new note record after normalizing its ParentType. EditedAt is automatically set via BeforeSave.

func (*Store) CreatePerson

func (s *Store) CreatePerson(p *Person, tagNames []string) error

CreatePerson inserts or updates a Person and (optionally) replaces its tags.

Behavior:

  • If p.ID == 0: insert; else: update the whole record via Save(p).
  • Tags: if tagNames is non-empty, tags are ensured/created and REPLACED on the person. (If tagNames is empty, tags are left untouched.)
  • Owner scoping: OwnerID is taken from the person record (p.OwnerID).

Atomicity: Executed in a single DB transaction; either everything succeeds or nothing is written.

func (*Store) CreateSignupToken

func (s *Store) CreateSignupToken(email, password string, ttl time.Duration, tokenPlain string) (*SignupToken, error)

CreateSignupToken: stores pending signup with token hash and optional password hash

func (*Store) CreateUser

func (s *Store) CreateUser(u *User) error

func (*Store) CreateZUGFeRDPDF

func (s *Store) CreateZUGFeRDPDF(inv *Invoice, ownerID uint, xmlpath string, pdfpath string, logger *slog.Logger) error

CreateZUGFeRDPDF creates a ZUGFeRD PDF file for the invoice. The XML is expected to exist at the given location and the PDF gets written to the location given by the last argument.

func (*Store) DeleteInvoice

func (s *Store) DeleteInvoice(inv *Invoice, ownerid any) error

DeleteInvoice removes an invoice and all referenced invoice positions from the database.

func (*Store) DeleteLetterheadTemplate

func (s *Store) DeleteLetterheadTemplate(id, ownerID uint) error

DeleteLetterheadTemplate deletes a template (regions auto-delete via CASCADE).

func (*Store) DeleteNote

func (s *Store) DeleteNote(id uint, ownerID uint, authorID uint) error

DeleteNote removes a note by ID, restricted to its owner and author. Authors can only delete their own notes.

func (*Store) DeletePhone

func (s *Store) DeletePhone(id any) error

DeletePhone deletes a single ContactInfo record by its ID.

It first ensures the record exists, then performs a delete (soft delete if GORM soft deletes are active on this model). Returns an error if not found or failed.

func (*Store) DeletePhoneWithCompanyIDAndOwnerID

func (s *Store) DeletePhoneWithCompanyIDAndOwnerID(companyid any, ownerid any) error

DeletePhoneWithCompanyIDAndOwnerID deletes all ContactInfo records of type "companies" for a given company ID and owner ID. Used to clean up contacts on company deletion.

func (*Store) DeletePhoneWithPersonIDAndOwnerID

func (s *Store) DeletePhoneWithPersonIDAndOwnerID(personid any, ownerid any) error

DeletePhoneWithPersonIDAndOwnerID deletes all ContactInfo records linked to a specific person.

func (*Store) DeleteUnusedTagsByIDs

func (s *Store) DeleteUnusedTagsByIDs(tx *gorm.DB, ownerID uint, tagIDs []uint) error

DeleteUnusedTagsByIDs removes tags for this owner that are in the provided ID set and are no longer referenced by tag_links. Safe to call inside the same transaction. Hard-delete unused tags among the provided IDs (owner-scoped).

func (*Store) DepartPerson

func (s *Store) DepartPerson(personID uint, ownerID uint, authorID uint) (*DepartPersonResult, error)

DepartPerson marks a person as having left their company and creates a note on the company.

This operation:

  • Sets DepartedAt to the current time
  • Creates a note on the associated company documenting the departure
  • The person remains linked to the company for historical reference

Parameters:

  • personID: ID of the person departing
  • ownerID: owner scope for authorization
  • authorID: user creating the departure record (for the note)

Returns ErrNotAllowed if the person doesn't belong to the owner. Returns an error if the person has no associated company.

func (*Store) EnsureDefaultLetterheadRegions

func (s *Store) EnsureDefaultLetterheadRegions(templateID, ownerID uint, pageWidthCm, pageHeightCm float64) error

EnsureDefaultLetterheadRegions makes sure the three fixed regions exist for the template. It creates missing ones with sane defaults, but does not delete anything.

func (*Store) FilterCompaniesByAnyTag

func (s *Store) FilterCompaniesByAnyTag(ownerID uint, tagNames []string) ([]Company, error)

FilterCompaniesByAnyTag returns companies that have at least one of the given tag names. Case-insensitive via Norm matching.

func (*Store) FilterPersonsByAnyTag

func (s *Store) FilterPersonsByAnyTag(ownerID uint, tagNames []string) ([]Person, error)

FilterPersonsByAnyTag returns persons that have at least one of the given tag names.

func (*Store) FindAllCompaniesWithText

func (s *Store) FindAllCompaniesWithText(search string, ownerid uint) ([]*Company, error)

FindAllCompaniesWithText performs a case-insensitive substring search on company names within an owner scope. Uses ILIKE on PostgreSQL and LOWER(name) LIKE on other dialects. ContactInfos are preloaded for convenience.

func (*Store) FindAllPeopleWithText

func (s *Store) FindAllPeopleWithText(search string, ownerid uint) ([]*Person, error)

FindAllPeopleWithText performs a case-insensitive substring search on person names within an owner scope. Uses ILIKE on PostgreSQL; uses LOWER(name) LIKE on other dialects.

func (*Store) FindInvitationByToken

func (s *Store) FindInvitationByToken(ctx context.Context, token string) (*Invitation, error)

FindInvitationByToken looks up an invitation by its token. - Returns (nil, nil) if no invitation exists for the token. - Returns (*Invitation, nil) on success. - Returns a non-nil error for database errors.

func (*Store) FindInvoices

func (s *Store) FindInvoices(ownerID uint, statuses []InvoiceStatus, companyID *uint, field string, from, to *time.Time, limit, offset int, order string) (rows []Invoice, total int64, err error)

func (*Store) GetActivityHeads

func (s *Store) GetActivityHeads(userID any, limit int) ([]ActivityHead, error)

GetActivityHeads returns the most recent items across all major entity types (companies, invoices, notes) for a given owner/user, ordered by creation time descending.

Internally this uses a SQL UNION to merge multiple tables into a unified feed. This avoids complex ORM joins and is efficient for SQLite (and other simple dialects).

Parameters:

  • userID: owner/tenant identifier (scopes the query)
  • limit: max number of feed items to return (defaults to 20 if <= 0)

func (*Store) GetInvoiceByOwner

func (s *Store) GetInvoiceByOwner(ownerID uint, id uint) (*Invoice, error)

GetInvoiceByOwner loads a single invoice by id, ensuring it belongs to the given owner. Returns gorm.ErrRecordNotFound when the invoice does not exist within the owner scope.

func (*Store) GetMaxCounter

func (s *Store) GetMaxCounter(companyID uint, useLocalCounter bool, ownerID uint) (uint, error)

GetMaxCounter returns the maximum counter for the given company

func (*Store) GetNoteByID

func (s *Store) GetNoteByID(id uint, ownerID uint) (*Note, error)

GetNoteByID loads a single note by ID, ensuring it belongs to the given owner.

func (*Store) GetRecentItems

func (s *Store) GetRecentItems(userID uint, limit int) ([]RecentItem, error)

GetRecentItems retrieves the most recently viewed items for a user, limited by the specified number

func (*Store) GetUserByEMail

func (s *Store) GetUserByEMail(email string) (*User, error)

func (*Store) GetUserByID

func (s *Store) GetUserByID(id any) (*User, error)

func (*Store) GetUserByResetToken

func (s *Store) GetUserByResetToken(token string) (*User, error)

Find user by plaintext token – validates expiry + constant-time compare

func (*Store) GetUserByResetTokenHashPrefix

func (s *Store) GetUserByResetTokenHashPrefix(fullHash []byte, prefixLen int) (*User, error)

GetUserByResetTokenHashPrefix looks up a user by a prefix of the SHA-256 hash of the reset token. This allows short links (using only part of the hash), while still verifying the full hash afterward for security.

Supported databases:

  • PostgreSQL: uses encode(bytea, 'hex')
  • MySQL/MariaDB: uses HEX() and LOWER()
  • SQLite: uses hex() and lower()

It performs a prefix match in the database, then verifies the full hash in constant time to avoid timing side-channel attacks.

func (*Store) InvoicesByIDs

func (s *Store) InvoicesByIDs(ownerID any, ids []uint) (map[uint]Invoice, error)

func (*Store) ListAPITokensByOwner

func (s *Store) ListAPITokensByOwner(ownerID uint, limit int, cursor string) ([]APIToken, string, error)

ListAPITokensByOwner returns a paginated list of API tokens for a given owner.

Parameters:

  • ownerID: the account or tenant owning the tokens
  • limit: number of records to fetch (1–200, default 50)
  • cursor: offset-based pagination cursor (as string)

Returns:

  • slice of APITokens (up to limit)
  • next cursor string (empty if no more records)
  • error if query fails

Tokens are ordered by creation date (most recent first).

func (*Store) ListAllCompaniesByTags

func (s *Store) ListAllCompaniesByTags(ownerID uint, f CompanyListFilters) ([]Company, error)

ListAllCompaniesByTags returns all companies matching the given filters (no external pagination). Internally iterates in fixed-size pages to control memory usage.

func (*Store) ListCompaniesForExportCtx

func (s *Store) ListCompaniesForExportCtx(
	ctx context.Context,
	ownerID uint,
) ([]Company, error)

func (*Store) ListInvitations

func (s *Store) ListInvitations(ctx context.Context) ([]Invitation, error)

ListInvitations returns all invitations ordered by creation time (newest first).

func (*Store) ListInvoices

func (s *Store) ListInvoices(ownerID uint, q InvoiceListQuery) (items []Invoice, nextCursor string, err error)

ListInvoices returns a page of invoices for the given owner along with the next cursor. Owner-scoped and safe to call repeatedly for pagination.

Paging model:

  • Uses an offset-based cursor encoded as a string (q.Cursor).
  • Fetches Limit+1 rows to determine if there is a next page; if so, trims to Limit and returns nextCursor = offset + Limit (as string).

Filters:

  • Status (exact match)
  • CompanyID

Sorting:

  • "date_desc" (default): ORDER BY date DESC
  • "date_asc": ORDER BY date ASC
  • "created_desc": ORDER BY created_at DESC

func (*Store) ListInvoicesForExport

func (s *Store) ListInvoicesForExport(ownerID uint) ([]Invoice, error)

func (*Store) ListLetterheadTemplates

func (s *Store) ListLetterheadTemplates(ownerID uint) ([]LetterheadTemplate, error)

ListLetterheadTemplates returns all templates for a given owner.

func (*Store) ListLetterheadTemplatesForExportCtx

func (s *Store) ListLetterheadTemplatesForExportCtx(
	ctx context.Context,
	ownerID uint,
) ([]LetterheadTemplate, error)

func (*Store) ListNotesForParent

func (s *Store) ListNotesForParent(ownerID uint, parentType ParentType, parentID uint, f NoteFilters) ([]Note, error)

ListNotesForParent returns a list of notes belonging to a given parent entity, optionally filtered by search terms, with pagination support.

Search applies a simple LIKE filter over title, body, and tags (case-sensitive by default).

func (*Store) ListOwnerCompanyTags

func (s *Store) ListOwnerCompanyTags(ownerID uint) ([]TagCount, error)

ListOwnerCompanyTags returns all tag names used on companies for a given owner with usage counts. Soft-deleted links are ignored.

func (*Store) ListPersonsForExportCtx

func (s *Store) ListPersonsForExportCtx(
	ctx context.Context,
	ownerID uint,
) ([]Person, error)

ListPersonsForExportCtx returns all persons for the given owner, preloading ContactInfos and Notes. Used for data export.

func (*Store) ListTagsForParent

func (s *Store) ListTagsForParent(ownerID uint, parentType ParentType, parentID uint) ([]Tag, error)

ListTagsForParent returns the Tag list for a given parent.

func (*Store) ListUsers

func (s *Store) ListUsers(q string, offset, limit int) ([]User, int64, error)

ListUsers returns a page of users filtered by query `q` (matches email or full name, case-insensitive). It also returns the total count for pagination.

func (*Store) LoadActivity

func (s *Store) LoadActivity(ownerID any, limit int) (*ActivityHydration, error)

LoadActivity loads the latest unified feed (activity heads) and preloads all referenced entities (companies, people, invoices, notes) in batch.

This method effectively produces a complete in-memory view of recent activity without issuing a separate SQL query per item.

func (*Store) LoadAllCompanies

func (s *Store) LoadAllCompanies(ownerid any) ([]*Company, error)

LoadAllCompanies returns all companies for a given owner, preloading ContactInfos. Use with care for large datasets (consider pagination).

func (*Store) LoadAllNotesForParent

func (s *Store) LoadAllNotesForParent(ownerID uint, parentType ParentType, parentID uint) ([]Note, error)

LoadAllNotesForParent is a convenience wrapper around ListNotesForParent using default (empty) filters.

func (*Store) LoadAndVerifyInvoice

func (s *Store) LoadAndVerifyInvoice(id any, ownerID uint) (*Invoice, []einvoice.SemanticError, error)

func (*Store) LoadCompany

func (s *Store) LoadCompany(id any, ownerID any) (*Company, error)

LoadCompany loads a company by (id, ownerID), including:

  • Invoices (ordered newest first),
  • ContactInfos,
  • Contacts (people) via a follow-up query.

func (*Store) LoadInvoice

func (s *Store) LoadInvoice(id any, ownerid uint) (*Invoice, error)

LoadInvoice loads an invoice

func (*Store) LoadInvoiceWithTemplate

func (s *Store) LoadInvoiceWithTemplate(id any, ownerid uint) (*Invoice, error)

func (*Store) LoadLetterheadTemplate

func (s *Store) LoadLetterheadTemplate(id, ownerID uint) (*LetterheadTemplate, error)

LoadLetterheadTemplate loads a template (including its regions) for a given owner.

func (*Store) LoadLetterheadTemplateAnyOwner

func (s *Store) LoadLetterheadTemplateAnyOwner(id uint) (*LetterheadTemplate, error)

func (*Store) LoadLetterheadTemplateForAccess

func (s *Store) LoadLetterheadTemplateForAccess(id, ownerID uint, isAdmin bool) (*LetterheadTemplate, error)

Optional Convenience: kapselt die Zugriffspolitik

func (*Store) LoadPeopleForCompany

func (s *Store) LoadPeopleForCompany(id any, ownerID any) ([]*Person, error)

LoadPeopleForCompany returns all contacts for a given company within an owner scope. ContactInfos are preloaded.

func (*Store) LoadPerson

func (s *Store) LoadPerson(id any, ownerID any) (*Person, error)

LoadPerson fetches a person by id within an owner scope. Preloads ContactInfos and Company for convenience.

func (*Store) LoadPhone

func (s *Store) LoadPhone(phoneid any, ownerid any) (*ContactInfo, error)

LoadPhone loads a ContactInfo entry (of any type, not strictly “phone”) by its primary key and owner ID.

Returns the matching ContactInfo or a gorm.ErrRecordNotFound if not found.

func (*Store) LoadSettings

func (s *Store) LoadSettings(ownerID any) (*Settings, error)

LoadSettings loads the settings row for a given owner. Accepts ownerID as uint or int and returns an initialized (but unsaved) Settings record if none exists yet (via FirstOrInit).

func (*Store) LoadSettingsForExportCtx

func (s *Store) LoadSettingsForExportCtx(
	ctx context.Context,
	ownerID uint,
) (*Settings, error)

func (*Store) MarkInvoiceDraft

func (s *Store) MarkInvoiceDraft(id uint, ownerID uint, t time.Time) error

MarkInvoiceDraft rolls back an issued invoice to draft. Business rules: clears IssuedAt (and optionally Number/Counter).

func (*Store) MarkInvoiceIssued

func (s *Store) MarkInvoiceIssued(id uint, ownerID uint, t time.Time) error

Convenience: draft -> issued

func (*Store) MarkInvoicePaid

func (s *Store) MarkInvoicePaid(id uint, ownerID uint, t time.Time) error

Convenience: (draft|issued) -> paid

func (*Store) MaybeLiftCustomerCounterFor

func (s *Store) MaybeLiftCustomerCounterFor(ctx context.Context, num string) error

MaybeLiftCustomerCounterFor raises the settings counter if num's numeric part is ahead.

func (*Store) NextCustomerNumberTx

func (s *Store) NextCustomerNumberTx(ctx context.Context) (string, int64, error)

NextCustomerNumberTx allocates the next unique customer number in a transaction. Returns the formatted string and the numeric value used.

func (*Store) NotesByIDs

func (s *Store) NotesByIDs(ownerID any, ids []uint) (map[uint]Note, error)

func (*Store) PeopleByIDs

func (s *Store) PeopleByIDs(ownerID any, ids []uint) (map[uint]Person, error)

func (*Store) ReactivatePerson

func (s *Store) ReactivatePerson(personID uint, ownerID uint) error

ReactivatePerson removes the departed status from a person. Use this if a person returns to the company or was marked departed by mistake.

func (*Store) RemovePerson

func (s *Store) RemovePerson(id any, ownerID any) error

RemovePerson deletes a person if it belongs to the given owner. Returns ErrNotAllowed when the owner check fails.

func (*Store) ReplaceCompanyTagsByName

func (s *Store) ReplaceCompanyTagsByName(companyID, ownerID uint, names []string) error

func (*Store) ReplacePersonTagsByName

func (s *Store) ReplacePersonTagsByName(personID, ownerID uint, names []string) error

func (*Store) RevokeAPIToken

func (s *Store) RevokeAPIToken(ownerID, tokenID uint) error

RevokeAPIToken disables a token by marking it as "disabled". Only allowed for tokens belonging to the specified owner.

func (*Store) RevokeUserAccessImmediate

func (s *Store) RevokeUserAccessImmediate(ctx context.Context, userID uint) error

RevokeUserAccessImmediate invalidates all access vectors for a user immediately. Strategy:

  1. Delete API tokens (or mark revoked). 2a) If you store sessions server-side: delete them. 2b) If you use cookie-only sessions: bump SessionVersion so middleware rejects old cookies.

func (*Store) SaveCompany

func (s *Store) SaveCompany(c *Company, ownerID uint, tagNames []string) error

SaveCompany upserts a company, fully replaces its ContactInfos, and replaces its tags. Transactional and owner-scoped.

Semantics:

  • ContactInfos: "replace" semantics → delete existing rows, then insert the provided set.
  • Tags: tagNames==nil → keep; len==0 → remove all; len>0 → replace exactly with provided set.

func (*Store) SaveInvoice

func (s *Store) SaveInvoice(inv *Invoice, ownerid uint) error

SaveInvoice saves an invoice and all invoice positions SaveInvoice: robust against duplicates

func (*Store) SaveLetterheadTemplate

func (s *Store) SaveLetterheadTemplate(t *LetterheadTemplate, ownerID uint) error

SaveLetterheadTemplate creates or updates a letterhead template. Ownership is enforced.

func (*Store) SavePerson

func (s *Store) SavePerson(p *Person, ownerID uint, tagNames []string) error

SavePerson upserts a person, fully replaces its ContactInfos, and replaces its tags. Transactional and owner-scoped.

Semantics:

  • Upsert:
  • New person (p.ID == 0): insert.
  • Existing person: update whitelisted fields (name, e_mail, position, company_id) but only within the given owner scope.
  • ContactInfos: "replace" semantics. Existing rows are deleted, then the provided set is inserted.
  • Tags:
  • tagNames == nil -> leave existing tags unchanged
  • len(tagNames) == 0 -> remove all tags
  • len(tagNames) > 0 -> replace with exactly these tags

Security/scope: Operation is rejected if p.OwnerID != ownerID.

func (*Store) SaveSettings

func (s *Store) SaveSettings(settings *Settings) error

SaveSettings performs an upsert keyed by owner_id (ON CONFLICT DO UPDATE). If a row for owner_id exists, the listed columns are updated; otherwise, a new row is inserted.

Caveat: GORM translates ON CONFLICT per dialect. Ensure a unique index exists on owner_id (declared on the struct) and that the target DB supports the clause.

func (*Store) SearchCompaniesByTags

func (s *Store) SearchCompaniesByTags(ownerID uint, f CompanyListFilters) (CompanyListResult, error)

SearchCompaniesByTags performs a filtered search with pagination. Notes: - Tag names are normalized via normalizeTagName() -> match via tags.norm - ModeAND is handled by a HAVING count(distinct tag_id) = len(tags) - Query matches company name and (optional) email/domain fields if you have them

func (*Store) SetPassword

func (s *Store) SetPassword(u *User, password string) error

func (*Store) SetPasswordResetToken

func (s *Store) SetPasswordResetToken(u *User, token string, expiry time.Time) error

SetPasswordResetToken generates and stores a SHA-256 hash of the given token, along with an expiry timestamp. The token itself is *not* stored in plain text. This function works with PostgreSQL, SQLite, and MySQL/MariaDB.

func (*Store) SoftDeleteUserAccount

func (s *Store) SoftDeleteUserAccount(ctx context.Context, userID uint) error

SoftDeleteUserAccount marks the user as soft-deleted and sets a purge deadline. It does NOT purge domain data; a background job should hard-delete after the grace period.

func (*Store) SuggestNextCustomerNumber

func (s *Store) SuggestNextCustomerNumber(ctx context.Context) (string, error)

SuggestNextCustomerNumber returns a non-persistent suggestion (counter+1 formatted).

func (*Store) SuggestTagNames

func (s *Store) SuggestTagNames(ownerID uint, prefix string, limit int) ([]string, error)

SuggestTagNames is a convenience that returns only the display names.

func (*Store) SuggestTags

func (s *Store) SuggestTags(ownerID uint, prefix string, limit int) ([]Tag, error)

SuggestTags returns tags for an owner whose normalized form starts with the given prefix. It filters out soft-deleted rows and orders by display name (Name) ascending. If limit <= 0, a sensible default is used.

func (*Store) TagsForCompanies

func (s *Store) TagsForCompanies(ownerID uint, ids []uint) (map[uint][]Tag, error)

TagsForCompanies returns a map[companyID][]Tag for the given company IDs. Skips soft-deleted tag links and orders tags case-insensitively by name.

func (*Store) TouchLastLogin

func (s *Store) TouchLastLogin(u *User) error

func (*Store) TouchRecentView

func (s *Store) TouchRecentView(userID uint, et EntityType, entityID uint) error

TouchRecentView updates or creates a recent view entry for the given user and entity

func (*Store) UpdateInvoice

func (s *Store) UpdateInvoice(inv *Invoice, ownerid uint) error

UpdateInvoice updates an invoice and fully replaces its positions (hard delete + recreate).

func (*Store) UpdateLetterheadPageSize

func (s *Store) UpdateLetterheadPageSize(id, ownerID uint, wcm, hcm float64) error

UpdateLetterheadPageSize updates page size (in cm) of a template.

func (*Store) UpdateLetterheadPreviewURLs

func (s *Store) UpdateLetterheadPreviewURLs(id, ownerID uint, page1URL, page2URL string) error

UpdateLetterheadPreviewURLs updates preview image URLs.

func (*Store) UpdateLetterheadRegionsAndFonts

func (s *Store) UpdateLetterheadRegionsAndFonts(
	templateID, ownerID uint,
	regions []PlacedRegion,
	fonts *TemplateFonts,
	pageW, pageH float64,
) error

UpdateLetterheadRegionsAndFonts speichert Regions und zusätzlich Template-Meta (Fonts + Page-Size) atomar in einer Transaktion.

func (*Store) UpdateNoteContentAsAuthor

func (s *Store) UpdateNoteContentAsAuthor(ownerID, authorID, noteID uint, title, body, tags string) (*Note, error)

UpdateNoteContentAsAuthor allows the author of a note to update its content. Enforces that the current author matches the note's AuthorID.

Only title, body, tags, and edited_at are updated.

func (*Store) UpdateSettings

func (s *Store) UpdateSettings(settings *Settings) error

UpdateSettings updates fields for the existing row identified by owner_id. Uses an explicit WHERE owner_id filter to avoid accidentally updating by the primary key (ID) if the struct carries a different ID value.

Note: updated_at uses NOW() which is DB-specific; for SQLite you may prefer CURRENT_TIMESTAMP or let GORM manage timestamps automatically.

func (*Store) UpdateUser

func (s *Store) UpdateUser(u *User) error

func (*Store) ValidateAPIToken

func (s *Store) ValidateAPIToken(raw string) (*APIToken, error)

ValidateAPIToken verifies an incoming raw token string.

Validation steps:

  1. Check prefix length (minimum 12 characters).
  2. Look up the token by its prefix.
  3. Recompute and compare the salted SHA-256 hash in constant time.
  4. Ensure the token is not disabled and not expired.
  5. Update its "last_used_at" timestamp (best-effort; errors ignored).

Returns:

  • The matching APIToken record if valid.
  • A specific error (ErrTokenInvalid, ErrTokenNotFound, ErrTokenDisabled, ErrTokenExpired) otherwise.

This method avoids timing attacks by using constant-time comparison (crypto/subtle).

func (*Store) VoidInvoice

func (s *Store) VoidInvoice(id uint, ownerID uint, t time.Time) error

Convenience: (draft|issued) -> voided

func (*Store) WriteZUGFeRDXML

func (s *Store) WriteZUGFeRDXML(inv *Invoice, ownerID any, path string) error

WriteZUGFeRDXML writes the ZUGFeRD XML file to the hard drive. The file name is the invoice id plus the extension ".xml".

type Tag

type Tag struct {
	ID        uint `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	// Composite index for lookups AND part of the unique constraint with Norm
	OwnerID uint   `gorm:"index:idx_tag_owner_name,priority:1;uniqueIndex:uniq_tag_per_owner,priority:1"`
	Name    string `gorm:"size:128;not null;index:idx_tag_owner_name,priority:2"`

	// Normalized tag for cross-DB case-insensitive uniqueness
	Norm string `gorm:"size:128;not null;uniqueIndex:uniq_tag_per_owner,priority:2"`
}

Tag represents a reusable label. We store both the display name (Name) and a normalized version (Norm) to enforce cross-DB case-insensitive uniqueness. Uniqueness is: (OwnerID, Norm).

func (Tag) TableName

func (Tag) TableName() string

type TagCount

type TagCount struct {
	Name  string `json:"name"`
	Count int64  `json:"count"`
}
type TagLink struct {
	ID        uint `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`

	// All NOT NULL and all part of the unique constraint
	OwnerID    uint       `gorm:"not null;uniqueIndex:uniq_tag_parent,priority:1"`
	TagID      uint       `gorm:"not null;index:idx_taglink_tag;uniqueIndex:uniq_tag_parent,priority:2"`
	ParentType ParentType `gorm:"size:32;not null;index:idx_taglink_parent,priority:1;uniqueIndex:uniq_tag_parent,priority:3"`
	ParentID   uint       `gorm:"not null;index:idx_taglink_parent,priority:2;uniqueIndex:uniq_tag_parent,priority:4"`

	Tag Tag `gorm:"constraint:OnDelete:CASCADE;"`
}

TagLink is a polymorphic join from an entity (ParentType, ParentID) to a Tag. Uniqueness is (OwnerID, TagID, ParentType, ParentID) so a given tag can be assigned only once to that specific entity.

func (TagLink) TableName

func (TagLink) TableName() string

type TaxAmount

type TaxAmount struct {
	Rate   decimal.Decimal
	Amount decimal.Decimal
}

TaxAmount collects the amount for each rate

type TemplateFonts

type TemplateFonts struct {
	Normal string
	Bold   string
	Italic string
}

type User

type User struct {
	gorm.Model
	Email               string `gorm:"uniqueIndex;not null"` // always stored lowercase
	FullName            string
	Password            string `gorm:"not null"`
	PasswordResetToken  []byte
	PasswordResetExpiry time.Time
	Verified            bool `gorm:"not null;default:false"`
	LastLoginAt         *time.Time
	OwnerID             uint
}

User represents an application user

func (*User) BeforeSave

func (u *User) BeforeSave(tx *gorm.DB) error

Normalize email before saving

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL