The ingest only ever wrote. A bill that reached pos_orders was safe and completely unreachable — no screen in the product could show it, and the only way to see a day's counter takings was to query the database by hand. Three endpoints: a paged bill list, one bill with its lines, and a summary split the three ways somebody actually asks for — by tender for reconciling a drawer, by day for a chart, by till for an outlet running several counters. locationid is required on all of them and is the authorisation boundary, so a caller cannot page through another shop's takings by omitting a parameter. Fetching a bill under the wrong outlet returns 404 even when the reference is a real one. Dates match businessdate rather than arrival, because a till that was offline overnight uploads yesterday's bills this morning and they belong to yesterday. The list is ordered by billedat for the same reason — sorting by arrival would interleave a recovered backlog through today. Unlike the ingest handlers these answer in the usual envelope: they are read by the web app, not by a terminal, and nothing about them is bound to the till's contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
2.5 KiB
Go
72 lines
2.5 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
|
|
"nearle/models"
|
|
"nearle/repositories"
|
|
)
|
|
|
|
type PosService interface {
|
|
IngestOrders(batch models.PosOrderBatch) (*models.PosAck, error)
|
|
IngestCustomers(batch models.PosCustomerBatch) (*models.PosAck, error)
|
|
Catalogue(storeID, since string, page, pageSize int) (*models.PosCatalogueResponse, error)
|
|
|
|
// RecordHealth stores one heartbeat. Never acknowledged back to the till:
|
|
// presence is a fire-and-forget signal, and a terminal that stopped selling
|
|
// because its heartbeat failed would be a worse outcome than a blank board.
|
|
RecordHealth(ctx context.Context, health models.PosHealth) error
|
|
|
|
TerminalHealth(ctx context.Context, terminalID string) (map[string]string, error)
|
|
LocationHealth(ctx context.Context, locationID string) ([]map[string]string, error)
|
|
|
|
Sales(f models.PosSalesFilter) (*models.PosSalesPage, error)
|
|
SaleDetail(locationID int, reference string) (*models.PosOrders, error)
|
|
SalesSummary(f models.PosSalesFilter) (*models.PosSalesSummary, error)
|
|
}
|
|
|
|
type posService struct {
|
|
repo repositories.PosRepository
|
|
presence repositories.PosPresenceRepository
|
|
}
|
|
|
|
func NewPosService(repo repositories.PosRepository, presence repositories.PosPresenceRepository) PosService {
|
|
return &posService{repo: repo, presence: presence}
|
|
}
|
|
|
|
func (s *posService) RecordHealth(ctx context.Context, health models.PosHealth) error {
|
|
return s.presence.Record(ctx, health)
|
|
}
|
|
|
|
func (s *posService) TerminalHealth(ctx context.Context, terminalID string) (map[string]string, error) {
|
|
return s.presence.Terminal(ctx, terminalID)
|
|
}
|
|
|
|
func (s *posService) LocationHealth(ctx context.Context, locationID string) ([]map[string]string, error) {
|
|
return s.presence.Location(ctx, locationID)
|
|
}
|
|
|
|
func (s *posService) IngestOrders(batch models.PosOrderBatch) (*models.PosAck, error) {
|
|
return s.repo.IngestOrders(batch)
|
|
}
|
|
|
|
func (s *posService) IngestCustomers(batch models.PosCustomerBatch) (*models.PosAck, error) {
|
|
return s.repo.IngestCustomers(batch)
|
|
}
|
|
|
|
func (s *posService) Catalogue(storeID, since string, page, pageSize int) (*models.PosCatalogueResponse, error) {
|
|
return s.repo.Catalogue(storeID, since, page, pageSize)
|
|
}
|
|
|
|
func (s *posService) Sales(f models.PosSalesFilter) (*models.PosSalesPage, error) {
|
|
return s.repo.Sales(f)
|
|
}
|
|
|
|
func (s *posService) SaleDetail(locationID int, reference string) (*models.PosOrders, error) {
|
|
return s.repo.SaleDetail(locationID, reference)
|
|
}
|
|
|
|
func (s *posService) SalesSummary(f models.PosSalesFilter) (*models.PosSalesSummary, error) {
|
|
return s.repo.SalesSummary(f)
|
|
}
|