Documentation
¶
Index ¶
- Variables
- func IsValidUploadIntent(intent string) bool
- type AccountingRepoer
- type AssetRepoer
- type BackgroundJob
- type BranchRepoer
- type CompanyRepoer
- type Crudable
- type CrudableWithTx
- type CustomerRepoer
- type DiscountRepoer
- type Enqueuer
- type Event
- type EventEmitter
- type ExpenseRepoer
- type FileRepoer
- type FileStorage
- type IntentConfig
- type InventoryQuerier
- type InventoryRepoer
- type InvoiceRepoer
- type JournalEntryFilter
- type OrderFilter
- type OrderRepoer
- type POSStationRepoer
- type ProductRepoer
- type ReportsQuerier
- type SessionFilter
- type SessionRepoer
- type SupplierPaymentRepoer
- type SupplierRepoer
- type SupportsPagination
- type TxAware
- type URLPresigner
- type UploadIntent
- type UserRepoer
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNotFound indicates the requested resource does not exist. ErrNotFound = errors.New("resource not found") // ErrConflict indicates the operation conflicts with existing state. ErrConflict = errors.New("resource conflict") // ErrForbidden indicates the caller lacks permission for the operation. ErrForbidden = errors.New("forbidden: insufficient permissions") // ErrInternal indicates an unexpected internal failure. ErrInternal = errors.New("internal error") // ErrNoProcurementPrice is returned when COGS cannot be computed because // one or more ingredients have no procurement price on record. ErrNoProcurementPrice = errors.New("no procurement price on record") // ErrInsufficientStock is returned when there isn't enough stock to fulfill // a request — either because available units cannot be computed (one or // more ingredients have no stock record yet) or because the requested // quantity exceeds current stock/available units. ErrInsufficientStock = errors.New("insufficient stock") // ErrInvalidProductType is returned when an operation is attempted on a // product of the wrong type (e.g. setting a recipe on a simple product). ErrInvalidProductType = errors.New("invalid product type for operation") // ErrUnitCostRequired is returned when a stock movement of type procurement // or opening_balance is recorded without a unit_cost. ErrUnitCostRequired = errors.New("unit_cost is required for this movement reason") // ErrUnitCostForbidden is returned when a unit_cost is supplied for a stock // movement type that does not accept one (waste, adjustment_up, adjustment_down). ErrUnitCostForbidden = errors.New("unit_cost must not be set for this movement reason") // ErrSupplierRequiredForCreditProcurement is returned when a procurement is // recorded on credit (paid_immediately == false) without a supplier_id — // you can't owe a debt to nobody. ErrSupplierRequiredForCreditProcurement = errors.New( "supplier_id is required for procurement on credit", ) // ErrInvoiceAlreadyPaid is returned when a payment is recorded against an // invoice that's already fully paid. ErrInvoiceAlreadyPaid = errors.New("this invoice has already been paid in full") // ErrInvoiceOverpayment is returned when a payment amount exceeds an // invoice's remaining balance. ErrInvoiceOverpayment = errors.New("payment amount exceeds the invoice's remaining balance") // ErrInvoiceHasPayments is returned when attempting to correct a // procurement whose invoice already has a payment recorded — correcting // the total_amount could make it inconsistent with what's been paid. ErrInvoiceHasPayments = errors.New( "cannot correct a procurement whose invoice already has payments recorded", ) // ErrCorrectionCannotChangePaymentType is returned when a correction // would flip a procurement's paid_immediately value — this would require // either voiding an existing invoice or creating one after the fact, // neither of which is supported; re-enter the movement instead. ErrCorrectionCannotChangePaymentType = errors.New( "paid_immediately cannot be changed via a correction", ) // ErrInitialStockIncomplete is returned when a product is created with // only one of initial quantity / initial unit cost set — both or neither // are required, since together they form one procurement movement. ErrInitialStockIncomplete = errors.New( "initial quantity and initial unit cost must both be set, or neither", ) // ErrOpeningBalanceAlreadySet is returned when a branch already has an // opening-balance journal entry posted. ErrOpeningBalanceAlreadySet = errors.New("this branch already has an opening balance recorded") // ErrNoOpeningBalanceAmount is returned when every opening-balance amount // submitted is zero — nothing to post. ErrNoOpeningBalanceAmount = errors.New("enter at least one nonzero opening balance amount") // ErrStationBusy is returned when a session is opened on a station that // already has an open session. ErrStationBusy = errors.New("station already has an open session") // ErrSessionAlreadyClosed is returned when a close is attempted on a session // that is already in the closed state. ErrSessionAlreadyClosed = errors.New("session is already closed") // ErrOpenOrdersExist is returned when a close is attempted on a session that // still has orders in open status. ErrOpenOrdersExist = errors.New("session has open orders and cannot be closed") // ErrPaymentMismatch is returned when the sum of payment amounts does not // equal the order total. ErrPaymentMismatch = errors.New("payment total does not match order total") // ErrInvalidPaymentMethod is returned when a payment method is not in the // configured allowed set. ErrInvalidPaymentMethod = errors.New("payment method is not accepted") // ErrInvalidPayment is returned when a payment leg's tendered amount is // less than the amount being credited toward the order. ErrInvalidPayment = errors.New("tendered amount cannot be less than payment amount") // ErrOrderNotPaid is returned when an operation that requires a paid order // (void, return) is attempted on an order in another state. ErrOrderNotPaid = errors.New("order is not in paid status") // ErrOrderNotOpen is returned when an operation that requires an open order // (add item, checkout) is attempted on an order in another state, or when // an order is created on a closed session. ErrOrderNotOpen = errors.New("order is not in open status") // ErrOrderAlreadyVoided is returned when a void is attempted on an order // that is already voided. ErrOrderAlreadyVoided = errors.New("order is already voided") // ErrUnbalancedEntry is returned when a journal entry's debit total does not // equal its credit total (violates double-entry bookkeeping). ErrUnbalancedEntry = errors.New( "journal entry is unbalanced: debit total must equal credit total", ) // ErrDiscountExceedsTotal is returned when the computed discount amount equals // or exceeds the order item subtotal or order total it is being applied to. ErrDiscountExceedsTotal = errors.New("discount amount exceeds order total") // ErrDiscountCodeNotFound is returned when no discount matches a // cashier-entered coupon code for the order's company. ErrDiscountCodeNotFound = errors.New("discount code not found") // ErrDiscountInactive is returned when applying a discount that has // been deactivated. ErrDiscountInactive = errors.New("discount is not active") // ErrDiscountNotYetValid is returned when applying a discount before // its ValidFrom date. ErrDiscountNotYetValid = errors.New("discount is not valid yet") // ErrDiscountExpired is returned when applying a discount after its // ValidUntil date. ErrDiscountExpired = errors.New("discount has expired") // ErrDiscountExhausted is returned when applying a discount that has // already reached its MaxRedemptions cap. ErrDiscountExhausted = errors.New("discount has reached its redemption limit") // ErrInvalidIntent is returned when an upload is attempted against an // intent that isn't in the registered UploadIntent set. ErrInvalidIntent = errors.New("invalid upload intent") // ErrUnsupportedMimeType is returned when an uploaded file's declared or // sniffed content type isn't allowed for the requested intent. ErrUnsupportedMimeType = errors.New("unsupported file type for this upload intent") // ErrFileTooLarge is returned when an uploaded file exceeds the requested // intent's configured size limit. ErrFileTooLarge = errors.New("file exceeds the maximum size for this upload intent") // ErrCustomerNameRequired is returned when creating a customer without a name. ErrCustomerNameRequired = errors.New("name is required to create a customer") // ErrCustomerContactRequired is returned when creating a customer with // neither an email nor a phone number. ErrCustomerContactRequired = errors.New("email or phone is required to create a customer") // ErrNotCorrectable is returned when attempting to correct a movement whose // reason isn't a user-entered one (pos_sale/pos_void/pos_return/correction). ErrNotCorrectable = errors.New("this stock movement cannot be corrected") // ErrAlreadyCorrected is returned when attempting to correct a movement // that already has a reversal recorded against it. ErrAlreadyCorrected = errors.New("this stock movement has already been corrected") // ErrBranchNotInCompany is returned when a product's branch restriction // list includes a branch that does not belong to the product's company. ErrBranchNotInCompany = errors.New("branch does not belong to this company") // ErrRecipeFeatureDisabled is returned when creating a recipe or // ingredient product for a company that has not enabled recipes. ErrRecipeFeatureDisabled = errors.New( "recipe and ingredient products are disabled for this company — enable them in company settings first", ) // ErrRecipesInUse is returned when disabling a company's recipe feature // while it still has recipe or ingredient products. The message itself // is shown to the user (see webhandler/errors.go's errMessage), so it // doubles as the explanation of why the change was rejected and what to // do about it. ErrRecipesInUse = errors.New( "cannot disable recipes while this company still has recipe or ingredient products — delete or convert them to Simple products first", ) // ErrSalvageValueTooHigh is returned when a fixed asset's salvage value // is not strictly less than its purchase cost — straight-line // depreciation would compute a zero-or-negative depreciable amount. ErrSalvageValueTooHigh = errors.New("salvage value must be less than purchase cost") // ErrAssetLocked is returned when attempting to change a fixed asset's // purchase cost, salvage value, useful life, or purchase date after // depreciation has already been posted against it — those fields are // exactly what past postings were computed from, so changing them would // silently misstate history. Dispose and re-register instead. ErrAssetLocked = errors.New( "cannot change cost, salvage value, useful life, or purchase date once depreciation has been posted for this asset", ) // ErrAssetAlreadyDisposed is returned when attempting to dispose of a // fixed asset that is not currently active. ErrAssetAlreadyDisposed = errors.New("this asset has already been disposed") // ErrAssetTrackingInUse is returned when disabling a company's asset // tracking while it still has fixed assets (active or disposed) on // record — unlike EnableAdvancedAccounting (which only hides a view), // hiding this feature would orphan real ledger-linked data. ErrAssetTrackingInUse = errors.New( "cannot disable asset tracking while this company still has registered fixed assets", ) )
Functions ¶
func IsValidUploadIntent ¶
IsValidUploadIntent reports whether intent is a registered upload intent.
Types ¶
type AccountingRepoer ¶
type AccountingRepoer interface {
// GetAccountByCode returns the account with the given COA code.
GetAccountByCode(ctx context.Context, code string) (*model.Account, error)
// ListAccounts returns all accounts ordered by code.
ListAccounts(ctx context.Context) ([]model.Account, error)
// CreateJournalEntry persists a JournalEntry together with all its JournalLines
// in a single atomic operation. The entry's ID and each line's ID are set before
// insertion; entry.CreatedAt is set to the current UTC time.
CreateJournalEntry(ctx context.Context, entry *model.JournalEntry) error
// ListJournalEntriesByCompany returns a paginated list of journal entries.
// Lines are NOT hydrated — call ListJournalLinesByEntry separately.
ListJournalEntriesByCompany(
ctx context.Context,
companyID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.JournalEntry], error)
// ListJournalEntriesByCompanyFiltered is the filterable counterpart to
// ListJournalEntriesByCompany, used by the ledger UI's journal-entries
// page. Lines are NOT hydrated here either.
ListJournalEntriesByCompanyFiltered(
ctx context.Context,
companyID uuid.UUID,
filter JournalEntryFilter,
q SupportsPagination,
) (*dto.PaginationResults[model.JournalEntry], error)
// BalanceByAccount returns the sum of debits and credits per COA account
// for all journal entries that belong to companyID.
BalanceByAccount(ctx context.Context, companyID uuid.UUID) ([]model.AccountBalance, error)
// HasOpeningBalanceEntry reports whether an opening-balance entry has
// already been posted for branchID.
HasOpeningBalanceEntry(ctx context.Context, companyID, branchID uuid.UUID) (bool, error)
// CountDepreciationPeriods counts how many "AssetDepreciationPosted"
// journal entries reference assetID — the authoritative source for both
// which periods have been posted (accounting.Service.PostAssetDepreciation's
// catch-up logic) and whether an asset's cost/salvage/life fields are
// locked (once count > 0). See accounting-implementation-plan.md Phase 7a.
CountDepreciationPeriods(ctx context.Context, assetID uuid.UUID) (int, error)
}
AccountingRepoer persists chart-of-accounts data and double-entry journal entries.
type AssetRepoer ¶ added in v0.5.1
type AssetRepoer interface {
Create(ctx context.Context, asset *model.FixedAsset) error
GetByID(ctx context.Context, id uuid.UUID) (*model.FixedAsset, error)
ListByCompany(ctx context.Context, companyID uuid.UUID) ([]model.FixedAsset, error)
// ListActiveByBranch returns active (non-disposed) assets for a branch,
// used by accounting.Service.PostAssetDepreciation to find eligible assets.
ListActiveByBranch(
ctx context.Context,
companyID, branchID uuid.UUID,
) ([]model.FixedAsset, error)
// Update persists the full row — used for both editing an unlocked
// asset's fields and flipping Status/DisposedAt on disposal.
Update(ctx context.Context, asset *model.FixedAsset) error
// HasAssetsForCompany reports whether companyID has any fixed_assets
// row, active or disposed — backs the enable_asset_tracking
// disable guard.
HasAssetsForCompany(ctx context.Context, companyID uuid.UUID) (bool, error)
}
AssetRepoer persists fixed-asset registry rows. No WithTx is needed — every write here is single-table; the ledger (via AccountingRepoer) is the source of truth for depreciation history, not this repo.
type BackgroundJob ¶ added in v0.2.0
type BackgroundJob interface {
// Kind identifies which worker/handler should process the job.
Kind() string
}
BackgroundJob is a named, JSON-serializable background job payload.
type BranchRepoer ¶
type BranchRepoer interface {
GetByID(ctx context.Context, id uuid.UUID) (*model.Branch, error)
ListByCompany(
ctx context.Context,
companyID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.Branch], error)
Create(ctx context.Context, entity *model.Branch) error
Update(ctx context.Context, entity *model.Branch) error
Delete(ctx context.Context, id uuid.UUID) error
WithTx(ctx context.Context, fn func(BranchRepoer) error) error
}
BranchRepoer defines the persistence contract for branch records.
type CompanyRepoer ¶
type CompanyRepoer interface {
GetByID(ctx context.Context, id uuid.UUID) (*model.Company, error)
GetByHandle(ctx context.Context, handle string) (*model.Company, error)
ListByOwner(
ctx context.Context,
ownerID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.Company], error)
Create(ctx context.Context, entity *model.Company) error
Update(ctx context.Context, entity *model.Company) error
Delete(ctx context.Context, id uuid.UUID) error
WithTx(ctx context.Context, fn func(CompanyRepoer) error) error
}
CompanyRepoer defines the persistence contract for company records. List is scoped by owner_id because a user may own multiple companies (pro).
type Crudable ¶
type Crudable[T any] interface { GetByID(ctx context.Context, id uuid.UUID) (*T, error) Paginate(ctx context.Context, q SupportsPagination) (*dto.PaginationResults[T], error) Create(ctx context.Context, entity *T) error Update(ctx context.Context, entity *T) error Delete(ctx context.Context, id uuid.UUID) error }
Crudable is a generic interface for repositories that support standard CRUD operations.
type CrudableWithTx ¶
CrudableWithTx combines Crudable with transaction support. T is the entity type; R is the repo type passed inside the transaction.
type CustomerRepoer ¶
type CustomerRepoer interface {
GetByID(ctx context.Context, id uuid.UUID) (*model.Customer, error)
GetByCompanyAndEmail(
ctx context.Context,
companyID uuid.UUID,
email string,
) (*model.Customer, error)
GetByCompanyAndPhone(
ctx context.Context,
companyID uuid.UUID,
phone string,
) (*model.Customer, error)
ListByCompany(
ctx context.Context,
companyID uuid.UUID,
search string,
q SupportsPagination,
) (*dto.PaginationResults[model.Customer], error)
Create(ctx context.Context, entity *model.Customer) error
Update(ctx context.Context, entity *model.Customer) error
Delete(ctx context.Context, id uuid.UUID) error
WithTx(ctx context.Context, fn func(CustomerRepoer) error) error
}
CustomerRepoer defines the persistence contract for customer records.
type DiscountRepoer ¶
type DiscountRepoer interface {
Create(ctx context.Context, entity *model.Discount) error
GetByID(ctx context.Context, id uuid.UUID) (*model.Discount, error)
GetByCode(ctx context.Context, companyID uuid.UUID, code string) (*model.Discount, error)
ListByCompany(
ctx context.Context,
companyID uuid.UUID,
search string,
q SupportsPagination,
) (*dto.PaginationResults[model.Discount], error)
// ListSelectable returns company discounts that are active, uncoded,
// and currently within their validity window (if any) — what the POS
// dropdown may offer, computed in SQL rather than filtered in Go.
ListSelectable(ctx context.Context, companyID uuid.UUID, at time.Time) ([]model.Discount, error)
Update(ctx context.Context, entity *model.Discount) error
Delete(ctx context.Context, id uuid.UUID) error
CountRedemptions(ctx context.Context, discountID uuid.UUID) (int, error)
CreateRedemption(ctx context.Context, r *model.DiscountRedemption) error
DeleteRedemptionByOrder(ctx context.Context, orderID uuid.UUID) error
}
DiscountRepoer provides CRUD access to the named discount catalog.
type Enqueuer ¶
type Enqueuer interface {
// Enqueue adds a job to the queue. Non-transactional.
Enqueue(ctx context.Context, job BackgroundJob) error
}
Enqueuer enqueues background jobs. The OSS implementation wraps goqite (SQLite-backed). Pro injects its own via core.WithEnqueuer — e.g. River for PostgreSQL.
type EventEmitter ¶
EventEmitter dispatches domain events to subscribers. Implemented by the job queue in Phase 7; nil until then.
type ExpenseRepoer ¶
type ExpenseRepoer interface {
Create(ctx context.Context, entity *model.Expense) error
GetByID(ctx context.Context, id uuid.UUID) (*model.Expense, error)
ListByCompany(
ctx context.Context,
companyID uuid.UUID,
search string,
q SupportsPagination,
) (*dto.PaginationResults[model.Expense], error)
}
ExpenseRepoer defines the persistence contract for expense records.
type FileRepoer ¶
type FileRepoer interface {
Create(ctx context.Context, entity *model.File) error
GetByID(ctx context.Context, id, companyID uuid.UUID) (*model.File, error)
Delete(ctx context.Context, id, companyID uuid.UUID) error
}
FileRepoer provides CRUD access to uploaded file metadata.
type FileStorage ¶
type FileStorage interface {
Put(ctx context.Context, key string, r io.Reader, size int64) error
Get(ctx context.Context, key string) (io.ReadCloser, error)
Delete(ctx context.Context, key string) error
}
FileStorage stores and retrieves file bytes by key. Implementations never see or need an absolute path — the key is relative to the backend's root (e.g. a bucket, or a base directory on disk).
type IntentConfig ¶
type IntentConfig struct {
// Prefix is prepended to the generated filename to form the storage key,
// e.g. "avatars/" -> "<companyID>/avatars/<uuid>.webp".
Prefix string
AllowedMIMETypes []string
MaxSizeBytes int64
URLExpiry time.Duration
}
IntentConfig is the validation/config for one UploadIntent.
func LookupIntent ¶
func LookupIntent(intent UploadIntent) (IntentConfig, bool)
LookupIntent returns the config for a given intent, and whether it exists.
type InventoryQuerier ¶
type InventoryQuerier interface {
// LPP returns the Last Procurement Price for the given product (ingredient).
// Returns ErrNoProcurementPrice if no procurement stock movement exists yet.
LPP(ctx context.Context, productID uuid.UUID) (int64, error)
// LPPTrend returns the same value as LPP plus the procurement unit_cost
// before it, if any — used to render a cost trend indicator. previous is
// nil when there's no earlier procurement to compare against. Returns
// ErrNoProcurementPrice under the same condition as LPP.
LPPTrend(ctx context.Context, productID uuid.UUID) (current int64, previous *int64, err error)
// CurrentStock returns the current stock level (in product units) for the
// given ingredient product.
// Returns ErrInsufficientStock if no stock record exists yet.
CurrentStock(ctx context.Context, productID uuid.UUID) (float64, error)
// StockAt returns the stock level at the given point in time.
StockAt(ctx context.Context, productID uuid.UUID, at time.Time) (float64, error)
}
InventoryQuerier provides read-only inventory queries used by ProductService. Implemented by InventoryRepo in Phase 4; may be nil in Phase 3.
type InventoryRepoer ¶
type InventoryRepoer interface {
Record(ctx context.Context, entity *model.StockMovement) error
GetByID(ctx context.Context, id uuid.UUID) (*model.StockMovement, error)
ListByCompany(
ctx context.Context,
companyID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.StockMovement], error)
// ListByProduct returns paginated movements for productID, scoped to
// companyID so a caller cannot read another company's product history.
ListByProduct(
ctx context.Context,
productID, companyID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.StockMovement], error)
// CurrentStock returns the signed sum of all deltas for the product.
// Returns 0 when no movements exist (never ErrInsufficientStock at repo level).
CurrentStock(ctx context.Context, productID uuid.UUID) (float64, error)
StockAt(ctx context.Context, productID uuid.UUID, at time.Time) (float64, error)
// LPP returns the unit_cost of the most recent procurement movement.
// Returns ErrNoProcurementPrice when no procurement exists.
LPP(ctx context.Context, productID uuid.UUID) (int64, error)
// LPPTrend returns the same value as LPP plus the procurement unit_cost
// before it, if any. previous is nil when there's no earlier procurement
// to compare against.
LPPTrend(ctx context.Context, productID uuid.UUID) (current int64, previous *int64, err error)
// HasMovements reports whether any stock movement exists for the product.
HasMovements(ctx context.Context, productID uuid.UUID) (bool, error)
// IsReversed reports whether a correction movement already reverses movementID.
IsReversed(ctx context.Context, movementID uuid.UUID) (bool, error)
WithTx(ctx context.Context, fn func(InventoryRepoer) error) error
// CreateInvoice persists a procurement invoice. It lives here (not only
// on InvoiceRepoer) solely so RecordProcurement can create the
// stock_movement and its invoice atomically inside one WithTx.
CreateInvoice(ctx context.Context, entity *model.ProcurementInvoice) error
// GetInvoiceByID exists here as a transactionally-consistent re-check
// immediately before UpdateInvoiceTotalAmount — guards against a payment
// being recorded concurrently between CorrectMovement's outer pre-check
// (via InvoiceRepoer) and this write. For any other read, use InvoiceRepoer.
GetInvoiceByID(ctx context.Context, id uuid.UUID) (*model.ProcurementInvoice, error)
UpdateInvoiceTotalAmount(ctx context.Context, id uuid.UUID, totalAmount int64) error
}
InventoryRepoer defines the persistence contract for stock movements. It also satisfies InventoryQuerier (LPP + CurrentStock are present on both).
type InvoiceRepoer ¶ added in v0.5.1
type InvoiceRepoer interface {
GetByID(ctx context.Context, id uuid.UUID) (*model.ProcurementInvoice, error)
// ListOpenBySupplier returns open+partial invoices for a supplier,
// ordered by due_date (nulls last) then created_at — unpaginated, since
// a single supplier's outstanding invoice count is inherently small.
ListOpenBySupplier(
ctx context.Context,
supplierID uuid.UUID,
) ([]model.ProcurementInvoice, error)
ListBySupplier(
ctx context.Context,
supplierID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.ProcurementInvoice], error)
ListByCompany(
ctx context.Context,
companyID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.ProcurementInvoice], error)
}
InvoiceRepoer is the canonical read-side contract for procurement invoices — used for every non-transactional read (listing on the supplier page, any future report/API). Writes that must be atomic with a movement or payment write live on InventoryRepoer/SupplierPaymentRepoer instead, scoped to those services' own transactions.
type JournalEntryFilter ¶ added in v0.5.1
type JournalEntryFilter struct {
DateFrom string // YYYY-MM-DD, inclusive lower bound on entry_date
DateTo string // YYYY-MM-DD, inclusive upper bound on entry_date
BranchID *uuid.UUID
EventName string
}
JournalEntryFilter narrows ListJournalEntriesByCompanyFiltered. Zero values mean "no filter" on that dimension.
type OrderFilter ¶
type OrderFilter struct {
DateFrom string // "YYYY-MM-DD" inclusive; "" = no lower bound
DateTo string // "YYYY-MM-DD" inclusive; "" = no upper bound
Status string // model.OrderStatus value; "" = any status
BranchID uuid.UUID // uuid.Nil = any branch
StationID uuid.UUID // uuid.Nil = any station
}
OrderFilter holds optional filter criteria for ListOrdersByCompanyFiltered. Zero values mean "no filter" for that field.
type OrderRepoer ¶
type OrderRepoer interface {
// CreateOrder persists a new open order. Sets ID, CreatedAt, UpdatedAt.
CreateOrder(ctx context.Context, entity *model.Order) error
// GetOrderByID retrieves an order by its ID.
GetOrderByID(ctx context.Context, id uuid.UUID) (*model.Order, error)
// GetOpenOrderBySession retrieves the session's current open order
// directly by status, not by paginating recent orders — unlike
// ListOrdersBySession, it stays correct no matter how many orders the
// session has accumulated. Returns ErrNotFound if none is open.
GetOpenOrderBySession(ctx context.Context, sessionID uuid.UUID) (*model.Order, error)
// UpdateOrder persists mutable order fields (status, total, receipt_number,
// voided_by, voided_at, updated_at).
UpdateOrder(ctx context.Context, entity *model.Order) error
// DeleteOrder removes an order record entirely.
DeleteOrder(ctx context.Context, id uuid.UUID) error
// ListOrdersBySession returns paginated orders for a session.
ListOrdersBySession(
ctx context.Context,
sessionID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.Order], error)
// ListOrdersByCompany returns paginated orders for a company.
ListOrdersByCompany(
ctx context.Context,
companyID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.Order], error)
// ListOrdersByCompanyFiltered returns paginated orders for a company,
// narrowed by filter. Zero-value fields on filter impose no restriction.
ListOrdersByCompanyFiltered(
ctx context.Context,
companyID uuid.UUID,
filter *OrderFilter,
q SupportsPagination,
) (*dto.PaginationResults[model.Order], error)
// CountOrdersByCompanyAndDate returns the number of orders created by the
// company on the given date (format "YYYY-MM-DD"). Used for receipt numbering.
CountOrdersByCompanyAndDate(
ctx context.Context,
companyID uuid.UUID,
date string,
) (int64, error)
// CreateOrderItem persists a new order item. Sets ID.
CreateOrderItem(ctx context.Context, entity *model.OrderItem) error
// GetOrderItemByID retrieves an order item by its ID.
GetOrderItemByID(ctx context.Context, id uuid.UUID) (*model.OrderItem, error)
// ListOrderItemsByOrder returns all items for an order.
ListOrderItemsByOrder(ctx context.Context, orderID uuid.UUID) ([]model.OrderItem, error)
// DeleteOrderItem removes an order item.
DeleteOrderItem(ctx context.Context, id uuid.UUID) error
// UpdateOrderItem updates the quantity and subtotal of an existing order item.
UpdateOrderItem(ctx context.Context, entity *model.OrderItem) error
// CreatePayment persists a payment leg. Sets ID.
CreatePayment(ctx context.Context, entity *model.Payment) error
// ListPaymentsByOrder returns all payment legs for an order.
ListPaymentsByOrder(ctx context.Context, orderID uuid.UUID) ([]model.Payment, error)
// SumPaymentsByOrder returns the total of completed payments for an order.
SumPaymentsByOrder(ctx context.Context, orderID uuid.UUID) (int64, error)
// CreateOrderReturn persists a return record. Sets ID.
CreateOrderReturn(ctx context.Context, entity *model.OrderReturn) error
// GetOrderReturnByID retrieves a return by its ID.
GetOrderReturnByID(ctx context.Context, id uuid.UUID) (*model.OrderReturn, error)
// ListOrderReturnsByOrder returns all return records for an order.
ListOrderReturnsByOrder(ctx context.Context, orderID uuid.UUID) ([]model.OrderReturn, error)
// CreateOrderReturnItem persists a return item line. Sets ID.
CreateOrderReturnItem(ctx context.Context, entity *model.OrderReturnItem) error
// ListOrderReturnItemsByReturn returns all item lines for a return.
ListOrderReturnItemsByReturn(
ctx context.Context,
returnID uuid.UUID,
) ([]model.OrderReturnItem, error)
// WithTx runs fn inside a single database transaction, providing transactional
// views of both the order repo and the inventory repo. Used for Checkout, Void,
// and Return which must write to orders/payments and stock_movements atomically.
WithTx(ctx context.Context, fn func(orders OrderRepoer, inv InventoryRepoer) error) error
}
OrderRepoer defines persistence operations for orders, items, payments, and returns.
type POSStationRepoer ¶
type POSStationRepoer interface {
GetByID(ctx context.Context, id uuid.UUID) (*model.POSStation, error)
ListByCompany(
ctx context.Context,
companyID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.POSStation], error)
Create(ctx context.Context, entity *model.POSStation) error
Update(ctx context.Context, entity *model.POSStation) error
Delete(ctx context.Context, id uuid.UUID) error
WithTx(ctx context.Context, fn func(POSStationRepoer) error) error
}
POSStationRepoer defines the persistence contract for POS station records.
type ProductRepoer ¶
type ProductRepoer interface {
GetByID(ctx context.Context, id uuid.UUID) (*model.Product, error)
// GetBySKU retrieves a product by its SKU, scoped to companyID. Used by
// the POS barcode-scan-to-cart flow.
GetBySKU(ctx context.Context, companyID uuid.UUID, sku string) (*model.Product, error)
ListByCompany(
ctx context.Context,
companyID uuid.UUID,
search string,
q SupportsPagination,
) (*dto.PaginationResults[model.Product], error)
ListByCompanyAndType(
ctx context.Context,
companyID uuid.UUID,
t model.ProductType,
search string,
q SupportsPagination,
) (*dto.PaginationResults[model.Product], error)
// ListSellableByCompany returns paginated Simple+Recipe products (the
// types the POS product grid can add to a cart) as one correctly-ordered
// and counted set, rather than merging two independently-paginated lists.
ListSellableByCompany(
ctx context.Context,
companyID uuid.UUID,
search string,
q SupportsPagination,
) (*dto.PaginationResults[model.Product], error)
// ListSellableByCompanyForBranch is ListSellableByCompany further
// filtered to products visible at branchID: unrestricted products (zero
// product_branches rows) plus products explicitly restricted to
// branchID. Used by the POS product grid.
ListSellableByCompanyForBranch(
ctx context.Context,
companyID, branchID uuid.UUID,
search string,
q SupportsPagination,
) (*dto.PaginationResults[model.Product], error)
// GetBySKUForBranch is GetBySKU further filtered to products visible at
// branchID, used by the POS barcode-scan-to-cart flow so scanning
// enforces the same branch restriction as the product grid.
GetBySKUForBranch(
ctx context.Context,
companyID, branchID uuid.UUID,
sku string,
) (*model.Product, error)
// ListByCompanyFiltered lists products for a company with type,
// category, and branch as independent optional filters (nil = don't
// filter on that dimension). Backs the product list page's filter row.
ListByCompanyFiltered(
ctx context.Context,
companyID uuid.UUID,
productType *model.ProductType,
categoryID, branchID *uuid.UUID,
search string,
q SupportsPagination,
) (*dto.PaginationResults[model.Product], error)
// HasRecipeOrIngredientProducts reports whether a company has any
// product of type recipe or ingredient. Used to block disabling a
// company's recipe feature while such products still exist.
HasRecipeOrIngredientProducts(ctx context.Context, companyID uuid.UUID) (bool, error)
Create(ctx context.Context, entity *model.Product) error
Update(ctx context.Context, entity *model.Product) error
Delete(ctx context.Context, id uuid.UUID) error
GetRecipeComponents(ctx context.Context, recipeID uuid.UUID) ([]model.RecipeComponent, error)
SetRecipeComponents(
ctx context.Context,
recipeID uuid.UUID,
components []model.RecipeComponent,
) error
GetRecipeOverheads(ctx context.Context, recipeID uuid.UUID) ([]model.RecipeOverhead, error)
SetRecipeOverheads(
ctx context.Context,
recipeID uuid.UUID,
overheads []model.RecipeOverhead,
) error
// GetOrCreateCategories resolves each name to a company-scoped category,
// reusing an existing one on a case-insensitive match (normalized in
// SQL) or creating it. Blank/whitespace-only names are skipped.
GetOrCreateCategories(
ctx context.Context,
companyID uuid.UUID,
names []string,
) ([]model.Category, error)
ListCategoriesByCompany(ctx context.Context, companyID uuid.UUID) ([]model.Category, error)
GetProductCategories(ctx context.Context, productID uuid.UUID) ([]model.Category, error)
// SetProductCategories atomically replaces a product's full category set.
SetProductCategories(
ctx context.Context,
productID uuid.UUID,
categoryIDs []uuid.UUID,
) error
// GetProductBranches returns the branches a product is restricted to. An
// empty slice means the product is unrestricted (sellable at every
// branch of its company).
GetProductBranches(ctx context.Context, productID uuid.UUID) ([]model.Branch, error)
// SetProductBranches atomically replaces a product's full
// branch-restriction set. An empty/nil slice clears all restrictions,
// making the product unrestricted.
SetProductBranches(ctx context.Context, productID uuid.UUID, branchIDs []uuid.UUID) error
// WithTx runs fn inside a single database transaction, providing both
// repos so a product write and a stock movement (e.g. initial stock at
// creation) can commit atomically — mirrors OrderRepoer.WithTx.
WithTx(ctx context.Context, fn func(products ProductRepoer, inv InventoryRepoer) error) error
}
ProductRepoer defines the persistence contract for product records, including recipe sub-entities (components and overheads).
type ReportsQuerier ¶
type ReportsQuerier interface {
// branchID, when non-nil, restricts to orders placed at that branch.
SalesTotals(
ctx context.Context,
companyID uuid.UUID,
from, to string,
branchID *uuid.UUID,
) (model.SalesSummary, error)
SalesByPeriod(
ctx context.Context,
companyID uuid.UUID,
from, to, groupBy string,
branchID *uuid.UUID,
) ([]model.SalesPeriod, error)
RevenueByPaymentMethod(
ctx context.Context,
companyID uuid.UUID,
from, to string,
branchID *uuid.UUID,
) ([]model.PaymentMethodRevenue, error)
TopProductsByRevenue(
ctx context.Context,
companyID uuid.UUID,
from, to string,
branchID *uuid.UUID,
topN int,
) ([]model.TopProduct, error)
InventorySnapshot(
ctx context.Context,
companyID uuid.UUID,
) ([]model.InventoryLine, error)
// ExpiringSoon returns procurement batches whose expiry_date falls
// within their product's own ExpiryWarningDays. Not filtered by current
// stock here — callers filter out CurrentStock <= 0 rows themselves
// (see dashboard_handler.go), same as InventorySnapshot's LowStock split.
ExpiringSoon(
ctx context.Context,
companyID uuid.UUID,
) ([]model.ExpiringBatch, error)
// InventorySnapshotPaged is the paginated, searchable counterpart to
// InventorySnapshot, used by the web Inventory list page. productType
// and stockStatus are exact-match filters ("" = no filter); stockStatus
// is one of "low"/"out"/"ok", mirroring view/inventory/helpers.go's
// stockStatusLabel. categoryID nil means no category filter.
InventorySnapshotPaged(
ctx context.Context,
companyID uuid.UUID,
search string,
productType string,
categoryID *uuid.UUID,
stockStatus string,
q SupportsPagination,
) (*dto.PaginationResults[model.InventoryLine], error)
// branchID, when non-nil, restricts to journal entries posted for that
// branch (company-wide postings, e.g. supplier payments or unassigned
// expenses, are excluded from any specific branch's figures).
PLByPeriod(
ctx context.Context,
companyID uuid.UUID,
from, to string,
branchID *uuid.UUID,
) (model.PLSummary, error)
// TrialBalanceAsOf returns per-account debit/credit totals as of a given
// date, optionally scoped to one branch. asOf == "" means no upper bound
// (lifetime-to-date, matching AccountingRepoer.BalanceByAccount's
// behavior). Backs both the trial balance and balance sheet views.
TrialBalanceAsOf(
ctx context.Context,
companyID uuid.UUID,
asOf string,
branchID *uuid.UUID,
) ([]model.AccountBalance, error)
}
ReportsQuerier provides read-only aggregate queries for management reports. All implementations use raw SQL; no sqlc involvement.
type SessionFilter ¶
SessionFilter narrows ListOpenByCompanyFiltered. uuid.Nil means "any branch".
type SessionRepoer ¶
type SessionRepoer interface {
// Create persists a new open session.
Create(ctx context.Context, entity *model.POSSession) error
// GetByID retrieves a session by its ID.
GetByID(ctx context.Context, id uuid.UUID) (*model.POSSession, error)
// GetOpenByStation retrieves the open session for a station, if any.
// Returns ErrNotFound if no open session exists.
GetOpenByStation(ctx context.Context, stationID uuid.UUID) (*model.POSSession, error)
// GetOpenByUser retrieves the open session opened by userID within
// companyID, if any. Returns ErrNotFound if no open session exists.
GetOpenByUser(ctx context.Context, companyID, userID uuid.UUID) (*model.POSSession, error)
// Close marks a session as closed at the given time.
Close(ctx context.Context, sessionID uuid.UUID, closedAt time.Time) error
// ListByCompany returns paginated sessions for a company.
ListByCompany(
ctx context.Context,
companyID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.POSSession], error)
// ListOpenByCompanyFiltered returns paginated open sessions for a
// company, optionally narrowed by filter.BranchID.
ListOpenByCompanyFiltered(
ctx context.Context,
companyID uuid.UUID,
filter *SessionFilter,
q SupportsPagination,
) (*dto.PaginationResults[model.POSSession], error)
// CreateReconciliation persists a session reconciliation record.
CreateReconciliation(ctx context.Context, entity *model.SessionReconciliation) error
// GetReconciliationBySession retrieves the reconciliation for a session.
GetReconciliationBySession(
ctx context.Context,
sessionID uuid.UUID,
) (*model.SessionReconciliation, error)
// SumCashPayments returns the total cash-method payments taken in a session.
// Phase 6 stub: always returns 0 until the payments table exists.
SumCashPayments(ctx context.Context, sessionID uuid.UUID) (int64, error)
// HasOpenOrders returns true if the session has any orders in open status.
// Phase 6 stub: always returns false until the orders table exists.
HasOpenOrders(ctx context.Context, sessionID uuid.UUID) (bool, error)
// DeleteOpenOrders hard-deletes every open order for the session
// (cascading to their items via FK) and returns the number deleted.
DeleteOpenOrders(ctx context.Context, sessionID uuid.UUID) (int64, error)
}
SessionRepoer defines persistence operations for POS sessions and reconciliations.
type SupplierPaymentRepoer ¶
type SupplierPaymentRepoer interface {
Create(ctx context.Context, entity *model.SupplierPayment) error
GetByID(ctx context.Context, id uuid.UUID) (*model.SupplierPayment, error)
ListByCompany(
ctx context.Context,
companyID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.SupplierPayment], error)
ListBySupplier(
ctx context.Context,
supplierID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.SupplierPayment], error)
WithTx(ctx context.Context, fn func(SupplierPaymentRepoer) error) error
// GetInvoiceByID exists here as a transactionally-consistent re-check
// immediately before UpdateInvoicePaid — guards against a concurrent
// payment racing between RecordPayment's outer pre-check (via
// InvoiceRepoer) and this write. For any other read, use InvoiceRepoer.
GetInvoiceByID(ctx context.Context, id uuid.UUID) (*model.ProcurementInvoice, error)
UpdateInvoicePaid(ctx context.Context, id uuid.UUID, paidAmount int64, status string) error
}
SupplierPaymentRepoer defines the persistence contract for supplier payment records.
type SupplierRepoer ¶
type SupplierRepoer interface {
GetByID(ctx context.Context, id uuid.UUID) (*model.Supplier, error)
ListByCompany(
ctx context.Context,
companyID uuid.UUID,
search string,
q SupportsPagination,
) (*dto.PaginationResults[model.Supplier], error)
Create(ctx context.Context, entity *model.Supplier) error
Update(ctx context.Context, entity *model.Supplier) error
Delete(ctx context.Context, id uuid.UUID) error
WithTx(ctx context.Context, fn func(SupplierRepoer) error) error
}
SupplierRepoer defines the persistence contract for supplier records.
type SupportsPagination ¶
SupportsPagination is satisfied by any DTO that can describe a page of results.
type TxAware ¶
type TxAware[T any] interface { // WithTx executes fn within a database transaction. // If fn returns an error the transaction is rolled back; otherwise it is committed. WithTx(ctx context.Context, fn func(T) error) error }
TxAware is a generic interface for repositories that support transactions. T is the repo type passed to the transaction function.
type URLPresigner ¶
type URLPresigner interface {
PresignGET(ctx context.Context, key string, ttl time.Duration) (string, error)
}
URLPresigner is an optional capability of a FileStorage backend that can mint a time-limited, direct-to-backend download URL (e.g. S3's presigned GET). Backends without a native presign mechanism, like the local filesystem driver, don't implement this — callers type-assert for it and fall back to another download strategy when absent.
type UploadIntent ¶
type UploadIntent string
UploadIntent is a predefined, validated purpose for an uploaded file. It determines the storage key prefix and the upload constraints (allowed MIME types, max size, presigned URL lifetime).
const ( // IntentAvatar is a user's profile picture. IntentAvatar UploadIntent = "avatar" // IntentCompanyLogo is a company's logo, shown in the sidebar. IntentCompanyLogo UploadIntent = "company_logo" // IntentProductImage is a product's photo, shown on its edit form and // in the POS product grid. IntentProductImage UploadIntent = "product_image" )
type UserRepoer ¶
type UserRepoer interface {
CrudableWithTx[model.User, UserRepoer]
GetByEmail(ctx context.Context, email string) (*model.User, error)
CountByCompany(ctx context.Context, companyID uuid.UUID) (int64, error)
// CountByRole reports how many users currently have the given role —
// used to gate registration when config.SingleOwnerMode is on (cap the
// system to one owner total).
CountByRole(ctx context.Context, role auth.UserRole) (int64, error)
ListByCompany(
ctx context.Context,
companyID uuid.UUID,
q SupportsPagination,
) (*dto.PaginationResults[model.User], error)
// InvalidateTokens sets the user's token_valid_after cutoff to now,
// rejecting any JWT issued before this call (see model.User.TokenValidAfter).
InvalidateTokens(ctx context.Context, userID uuid.UUID) error
}
UserRepoer defines the interface for interacting with user data storage. Implementations include a SQLite repo (OSS), a pgx/Postgres repo (Pro), and test stubs/mocks in the stubrepo and mockrepo packages.