package services import ( "context" "time" "nearle/models" "nearle/repositories" "nearle/utils" ) 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) // Login authenticates a person against the same account store the web // console uses and mints the session a till carries for the trading day. Login(req models.PosLoginRequest) (*models.PosSession, error) // LocationAllowed is the authorisation check every other POS call rests on: // does the tenant in the caller's token actually own this outlet. LocationAllowed(tenantID, locationID int) (bool, 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) } // Login authenticates a terminal's operator and issues its session. // // The token is minted here rather than in the repository so that the signing // key stays out of the layer that talks to the database, and so a future change // of token format touches one function. func (s *posService) Login(req models.PosLoginRequest) (*models.PosSession, error) { session, err := s.repo.PosLogin(req) if err != nil { return nil, err } token, expires, err := utils.MintPosToken(utils.PosClaims{ Userid: session.Userid, Tenantid: session.Tenantid, Locationid: session.Locationid, Roleid: session.Roleid, Terminalid: req.Terminalid, }, time.Now()) if err != nil { return nil, err } session.Token = token session.Expiresat = expires.UTC().Format(time.RFC3339) return session, nil } func (s *posService) LocationAllowed(tenantID, locationID int) (bool, error) { return s.repo.PosLocationAllowed(tenantID, locationID) }