Documentation
¶
Overview ¶
model/api_token_service.go
model/invoice_service.go
file: src/go/model/models_tags.go
user_repo.go (Model snippet)
Index ¶
- Variables
- func BuildCustomerListURL(basePath string, q string, tags []string, modeAND bool, page, pageSize int) string
- func JoinTags(a []string) string
- func NormalizeEmail(s string) string
- func RunMaintenance(ctx context.Context, s *Store) error
- func SplitTags(s string) []string
- func WriteSettingsXML(outFilename string, s LetterheadSettings) ([]byte, error)
- type APIToken
- type ActivityHead
- type ActivityHydration
- type Company
- type CompanyListFilters
- type CompanyListResult
- type Config
- type ContactInfo
- type DepartPersonResult
- type EntityType
- type FieldKind
- type FontsBlock
- type Invitation
- type Invoice
- type InvoiceListQuery
- type InvoicePosition
- type InvoiceProblem
- type InvoiceStatus
- type LetterheadBlock
- type LetterheadSettings
- type LetterheadTemplate
- type Note
- type NoteFilters
- type ParentType
- type Person
- type PlacedRegion
- type RecentItem
- type RecentView
- type RegionExport
- type Settings
- type SignupToken
- type Store
- func (s *Store) AddTagsToCompanyByName(companyID, ownerID uint, names []string) error
- func (s *Store) AddTagsToPersonByName(personID, ownerID uint, names []string) error
- func (s *Store) AuthenticateUser(email, password string) (*User, error)
- func (s *Store) AutoMigrateTokens() error
- func (s *Store) CheckCustomerNumber(ctx context.Context, num string, excludeID uint) (ok bool, message string, err error)
- func (s *Store) CheckPassword(u *User, password string) bool
- func (s *Store) ClearPasswordResetToken(u *User) error
- func (s *Store) CompaniesByIDs(ownerID any, ids []uint) (map[uint]Company, error)
- func (s *Store) CompanyNamesByIDs(ownerID uint, ids []uint) (map[uint]string, error)
- func (s *Store) ConsumeSignupToken(tokenPlain string) (*User, error)
- func (s *Store) CreateAPIToken(ownerID uint, userID *uint, name, scope string, expiresAt *time.Time) (plain string, rec *APIToken, err error)
- func (s *Store) CreateInvitation(ctx context.Context, inv *Invitation) error
- func (s *Store) CreateNote(n *Note) error
- func (s *Store) CreatePerson(p *Person, tagNames []string) error
- func (s *Store) CreateSignupToken(email, password string, ttl time.Duration, tokenPlain string) (*SignupToken, error)
- func (s *Store) CreateUser(u *User) error
- func (s *Store) CreateZUGFeRDPDF(inv *Invoice, ownerID uint, xmlpath string, pdfpath string, ...) error
- func (s *Store) DeleteInvoice(inv *Invoice, ownerid any) error
- func (s *Store) DeleteLetterheadTemplate(id, ownerID uint) error
- func (s *Store) DeleteNote(id uint, ownerID uint, authorID uint) error
- func (s *Store) DeletePhone(id any) error
- func (s *Store) DeletePhoneWithCompanyIDAndOwnerID(companyid any, ownerid any) error
- func (s *Store) DeletePhoneWithPersonIDAndOwnerID(personid any, ownerid any) error
- func (s *Store) DeleteUnusedTagsByIDs(tx *gorm.DB, ownerID uint, tagIDs []uint) error
- func (s *Store) DepartPerson(personID uint, ownerID uint, authorID uint) (*DepartPersonResult, error)
- func (s *Store) EnsureDefaultLetterheadRegions(templateID, ownerID uint, pageWidthCm, pageHeightCm float64) error
- func (s *Store) FilterCompaniesByAnyTag(ownerID uint, tagNames []string) ([]Company, error)
- func (s *Store) FilterPersonsByAnyTag(ownerID uint, tagNames []string) ([]Person, error)
- func (s *Store) FindAllCompaniesWithText(search string, ownerid uint) ([]*Company, error)
- func (s *Store) FindAllPeopleWithText(search string, ownerid uint) ([]*Person, error)
- func (s *Store) FindInvitationByToken(ctx context.Context, token string) (*Invitation, error)
- func (s *Store) FindInvoices(ownerID uint, statuses []InvoiceStatus, companyID *uint, field string, ...) (rows []Invoice, total int64, err error)
- func (s *Store) GetActivityHeads(userID any, limit int) ([]ActivityHead, error)
- func (s *Store) GetInvoiceByOwner(ownerID uint, id uint) (*Invoice, error)
- func (s *Store) GetMaxCounter(companyID uint, useLocalCounter bool, ownerID uint) (uint, error)
- func (s *Store) GetNoteByID(id uint, ownerID uint) (*Note, error)
- func (s *Store) GetRecentItems(userID uint, limit int) ([]RecentItem, error)
- func (s *Store) GetUserByEMail(email string) (*User, error)
- func (s *Store) GetUserByID(id any) (*User, error)
- func (s *Store) GetUserByResetToken(token string) (*User, error)
- func (s *Store) GetUserByResetTokenHashPrefix(fullHash []byte, prefixLen int) (*User, error)
- func (s *Store) InvoicesByIDs(ownerID any, ids []uint) (map[uint]Invoice, error)
- func (s *Store) ListAPITokensByOwner(ownerID uint, limit int, cursor string) ([]APIToken, string, error)
- func (s *Store) ListAllCompaniesByTags(ownerID uint, f CompanyListFilters) ([]Company, error)
- func (s *Store) ListCompaniesForExportCtx(ctx context.Context, ownerID uint) ([]Company, error)
- func (s *Store) ListInvitations(ctx context.Context) ([]Invitation, error)
- func (s *Store) ListInvoices(ownerID uint, q InvoiceListQuery) (items []Invoice, nextCursor string, err error)
- func (s *Store) ListInvoicesForExport(ownerID uint) ([]Invoice, error)
- func (s *Store) ListLetterheadTemplates(ownerID uint) ([]LetterheadTemplate, error)
- func (s *Store) ListLetterheadTemplatesForExportCtx(ctx context.Context, ownerID uint) ([]LetterheadTemplate, error)
- func (s *Store) ListNotesForParent(ownerID uint, parentType ParentType, parentID uint, f NoteFilters) ([]Note, error)
- func (s *Store) ListOwnerCompanyTags(ownerID uint) ([]TagCount, error)
- func (s *Store) ListPersonsForExportCtx(ctx context.Context, ownerID uint) ([]Person, error)
- func (s *Store) ListTagsForParent(ownerID uint, parentType ParentType, parentID uint) ([]Tag, error)
- func (s *Store) ListUsers(q string, offset, limit int) ([]User, int64, error)
- func (s *Store) LoadActivity(ownerID any, limit int) (*ActivityHydration, error)
- func (s *Store) LoadAllCompanies(ownerid any) ([]*Company, error)
- func (s *Store) LoadAllNotesForParent(ownerID uint, parentType ParentType, parentID uint) ([]Note, error)
- func (s *Store) LoadAndVerifyInvoice(id any, ownerID uint) (*Invoice, []einvoice.SemanticError, error)
- func (s *Store) LoadCompany(id any, ownerID any) (*Company, error)
- func (s *Store) LoadInvoice(id any, ownerid uint) (*Invoice, error)
- func (s *Store) LoadInvoiceWithTemplate(id any, ownerid uint) (*Invoice, error)
- func (s *Store) LoadLetterheadTemplate(id, ownerID uint) (*LetterheadTemplate, error)
- func (s *Store) LoadLetterheadTemplateAnyOwner(id uint) (*LetterheadTemplate, error)
- func (s *Store) LoadLetterheadTemplateForAccess(id, ownerID uint, isAdmin bool) (*LetterheadTemplate, error)
- func (s *Store) LoadPeopleForCompany(id any, ownerID any) ([]*Person, error)
- func (s *Store) LoadPerson(id any, ownerID any) (*Person, error)
- func (s *Store) LoadPhone(phoneid any, ownerid any) (*ContactInfo, error)
- func (s *Store) LoadSettings(ownerID any) (*Settings, error)
- func (s *Store) LoadSettingsForExportCtx(ctx context.Context, ownerID uint) (*Settings, error)
- func (s *Store) MarkInvoiceDraft(id uint, ownerID uint, t time.Time) error
- func (s *Store) MarkInvoiceIssued(id uint, ownerID uint, t time.Time) error
- func (s *Store) MarkInvoicePaid(id uint, ownerID uint, t time.Time) error
- func (s *Store) MaybeLiftCustomerCounterFor(ctx context.Context, num string) error
- func (s *Store) NextCustomerNumberTx(ctx context.Context) (string, int64, error)
- func (s *Store) NotesByIDs(ownerID any, ids []uint) (map[uint]Note, error)
- func (s *Store) PeopleByIDs(ownerID any, ids []uint) (map[uint]Person, error)
- func (s *Store) ReactivatePerson(personID uint, ownerID uint) error
- func (s *Store) RemovePerson(id any, ownerID any) error
- func (s *Store) ReplaceCompanyTagsByName(companyID, ownerID uint, names []string) error
- func (s *Store) ReplacePersonTagsByName(personID, ownerID uint, names []string) error
- func (s *Store) RevokeAPIToken(ownerID, tokenID uint) error
- func (s *Store) RevokeUserAccessImmediate(ctx context.Context, userID uint) error
- func (s *Store) SaveCompany(c *Company, ownerID uint, tagNames []string) error
- func (s *Store) SaveInvoice(inv *Invoice, ownerid uint) error
- func (s *Store) SaveLetterheadTemplate(t *LetterheadTemplate, ownerID uint) error
- func (s *Store) SavePerson(p *Person, ownerID uint, tagNames []string) error
- func (s *Store) SaveSettings(settings *Settings) error
- func (s *Store) SearchCompaniesByTags(ownerID uint, f CompanyListFilters) (CompanyListResult, error)
- func (s *Store) SetPassword(u *User, password string) error
- func (s *Store) SetPasswordResetToken(u *User, token string, expiry time.Time) error
- func (s *Store) SoftDeleteUserAccount(ctx context.Context, userID uint) error
- func (s *Store) SuggestNextCustomerNumber(ctx context.Context) (string, error)
- func (s *Store) SuggestTagNames(ownerID uint, prefix string, limit int) ([]string, error)
- func (s *Store) SuggestTags(ownerID uint, prefix string, limit int) ([]Tag, error)
- func (s *Store) TagsForCompanies(ownerID uint, ids []uint) (map[uint][]Tag, error)
- func (s *Store) TouchLastLogin(u *User) error
- func (s *Store) TouchRecentView(userID uint, et EntityType, entityID uint) error
- func (s *Store) UpdateInvoice(inv *Invoice, ownerid uint) error
- func (s *Store) UpdateLetterheadPageSize(id, ownerID uint, wcm, hcm float64) error
- func (s *Store) UpdateLetterheadPreviewURLs(id, ownerID uint, page1URL, page2URL string) error
- func (s *Store) UpdateLetterheadRegionsAndFonts(templateID, ownerID uint, regions []PlacedRegion, fonts *TemplateFonts, ...) error
- func (s *Store) UpdateNoteContentAsAuthor(ownerID, authorID, noteID uint, title, body, tags string) (*Note, error)
- func (s *Store) UpdateSettings(settings *Settings) error
- func (s *Store) UpdateUser(u *User) error
- func (s *Store) ValidateAPIToken(raw string) (*APIToken, error)
- func (s *Store) VoidInvoice(id uint, ownerID uint, t time.Time) error
- func (s *Store) WriteZUGFeRDXML(inv *Invoice, ownerID any, path string) error
- type Tag
- type TagCount
- type TagLink
- type TaxAmount
- type TemplateFonts
- type User
Constants ¶
This section is empty.
Variables ¶
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") )
var ErrNoSettingsRow = errors.New("no settings row found")
ErrNoSettingsRow is returned when no settings row exists in the database.
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 ¶
JoinTags joins a slice of tag strings into a single comma-separated value, trimming extra spaces.
func NormalizeEmail ¶
NormalizeEmail lowercases and trims the email string
func RunMaintenance ¶
RunMaintenance executes housekeeping tasks. Make sure tasks are idempotent and safe to run multiple times.
func SplitTags ¶
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).
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"`
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 ¶
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 ¶
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 FontsBlock ¶
type Invitation ¶
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
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 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.
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 ¶
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 NewStoreFromDB ¶
NewStoreFromDB creates a Store from an existing GORM database connection. Useful for testing with in-memory databases.
func (*Store) AddTagsToCompanyByName ¶
Public helpers to add/replace tags by names (transactional).
func (*Store) AddTagsToPersonByName ¶
func (*Store) AuthenticateUser ¶
func (*Store) AutoMigrateTokens ¶
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) ClearPasswordResetToken ¶
func (*Store) CompaniesByIDs ¶
func (*Store) CompanyNamesByIDs ¶
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 ¶
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 ¶
CreateNote inserts a new note record after normalizing its ParentType. EditedAt is automatically set via BeforeSave.
func (*Store) CreatePerson ¶
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 (*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 ¶
DeleteInvoice removes an invoice and all referenced invoice positions from the database.
func (*Store) DeleteLetterheadTemplate ¶
DeleteLetterheadTemplate deletes a template (regions auto-delete via CASCADE).
func (*Store) DeleteNote ¶
DeleteNote removes a note by ID, restricted to its owner and author. Authors can only delete their own notes.
func (*Store) DeletePhone ¶
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 ¶
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 ¶
DeletePhoneWithPersonIDAndOwnerID deletes all ContactInfo records linked to a specific person.
func (*Store) DeleteUnusedTagsByIDs ¶
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 ¶
FilterCompaniesByAnyTag returns companies that have at least one of the given tag names. Case-insensitive via Norm matching.
func (*Store) FilterPersonsByAnyTag ¶
FilterPersonsByAnyTag returns persons that have at least one of the given tag names.
func (*Store) FindAllCompaniesWithText ¶
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 ¶
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 ¶
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 (*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 ¶
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 ¶
GetMaxCounter returns the maximum counter for the given company
func (*Store) GetNoteByID ¶
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) GetUserByResetToken ¶
Find user by plaintext token – validates expiry + constant-time compare
func (*Store) GetUserByResetTokenHashPrefix ¶
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 (*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 (*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 (*Store) ListLetterheadTemplates ¶
func (s *Store) ListLetterheadTemplates(ownerID uint) ([]LetterheadTemplate, error)
ListLetterheadTemplates returns all templates for a given owner.
func (*Store) ListLetterheadTemplatesForExportCtx ¶
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 ¶
ListOwnerCompanyTags returns all tag names used on companies for a given owner with usage counts. Soft-deleted links are ignored.
func (*Store) ListPersonsForExportCtx ¶
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 ¶
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 ¶
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 (*Store) LoadCompany ¶
LoadCompany loads a company by (id, ownerID), including:
- Invoices (ordered newest first),
- ContactInfos,
- Contacts (people) via a follow-up query.
func (*Store) LoadInvoice ¶
LoadInvoice loads an invoice
func (*Store) LoadInvoiceWithTemplate ¶
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 ¶
LoadPeopleForCompany returns all contacts for a given company within an owner scope. ContactInfos are preloaded.
func (*Store) LoadPerson ¶
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 ¶
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 (*Store) MarkInvoiceDraft ¶
MarkInvoiceDraft rolls back an issued invoice to draft. Business rules: clears IssuedAt (and optionally Number/Counter).
func (*Store) MarkInvoiceIssued ¶
Convenience: draft -> issued
func (*Store) MarkInvoicePaid ¶
Convenience: (draft|issued) -> paid
func (*Store) MaybeLiftCustomerCounterFor ¶
MaybeLiftCustomerCounterFor raises the settings counter if num's numeric part is ahead.
func (*Store) NextCustomerNumberTx ¶
NextCustomerNumberTx allocates the next unique customer number in a transaction. Returns the formatted string and the numeric value used.
func (*Store) NotesByIDs ¶
func (*Store) PeopleByIDs ¶
func (*Store) ReactivatePerson ¶
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 ¶
RemovePerson deletes a person if it belongs to the given owner. Returns ErrNotAllowed when the owner check fails.
func (*Store) ReplaceCompanyTagsByName ¶
func (*Store) ReplacePersonTagsByName ¶
func (*Store) RevokeAPIToken ¶
RevokeAPIToken disables a token by marking it as "disabled". Only allowed for tokens belonging to the specified owner.
func (*Store) RevokeUserAccessImmediate ¶
RevokeUserAccessImmediate invalidates all access vectors for a user immediately. Strategy:
- 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 ¶
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 ¶
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 ¶
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 ¶
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) SetPasswordResetToken ¶
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 ¶
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 ¶
SuggestNextCustomerNumber returns a non-persistent suggestion (counter+1 formatted).
func (*Store) SuggestTagNames ¶
SuggestTagNames is a convenience that returns only the display names.
func (*Store) SuggestTags ¶
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 ¶
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 (*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 ¶
UpdateInvoice updates an invoice and fully replaces its positions (hard delete + recreate).
func (*Store) UpdateLetterheadPageSize ¶
UpdateLetterheadPageSize updates page size (in cm) of a template.
func (*Store) UpdateLetterheadPreviewURLs ¶
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 ¶
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 (*Store) ValidateAPIToken ¶
ValidateAPIToken verifies an incoming raw token string.
Validation steps:
- Check prefix length (minimum 12 characters).
- Look up the token by its prefix.
- Recompute and compare the salted SHA-256 hash in constant time.
- Ensure the token is not disabled and not expired.
- 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 ¶
Convenience: (draft|issued) -> voided
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).
type TagLink ¶
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.
type TemplateFonts ¶
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