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) // Staff lists who may ring a bill at an outlet. Sent with the session and // available on its own, so a shop that hires someone mid-shift can pull them // down without signing the terminal out. Staff(tenantID, locationID int) ([]models.PosStaffMember, error) // Till staff management, all scoped to the caller's own outlet. CreateUser(tenantID, locationID, configID int, req models.PosUserRequest) (*models.PosUser, error) UpdateUser(tenantID, locationID int, req models.PosUserRequest) (*models.PosUser, error) ListUsers(tenantID, locationID int, includeInactive bool) ([]models.PosUser, error) DeactivateUser(tenantID, locationID, userID int) error // LoginWithPin signs a person in at a terminal that is already open. Never // reachable anonymously — four digits is not a barrier on its own. LoginWithPin(tenantID, locationID int, pin string) (*models.PosSession, error) // ConfigidFor infers which portal a tenant's people belong to, so the console // is never asked for a number nobody can look up. ConfigidFor(tenantID int) int } 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 } return s.mint(session, req.Terminalid) } // mint signs a resolved session. // // Kept apart from the credential checks so the signing key stays out of the // layer that talks to the database, and so a change of token format touches one // function rather than every way in. func (s *posService) mint(session *models.PosSession, terminalID string) (*models.PosSession, error) { token, expires, err := utils.MintPosToken(utils.PosClaims{ Userid: session.Userid, Tenantid: session.Tenantid, Locationid: session.Locationid, Roleid: session.Roleid, Configid: session.Configid, Terminalid: 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) } func (s *posService) Staff(tenantID, locationID int) ([]models.PosStaffMember, error) { return s.repo.PosStaff(tenantID, locationID) } func (s *posService) CreateUser(tenantID, locationID, configID int, req models.PosUserRequest) (*models.PosUser, error) { return s.repo.CreatePosUser(tenantID, locationID, configID, req) } func (s *posService) UpdateUser(tenantID, locationID int, req models.PosUserRequest) (*models.PosUser, error) { return s.repo.UpdatePosUser(tenantID, locationID, req) } func (s *posService) ListUsers(tenantID, locationID int, includeInactive bool) ([]models.PosUser, error) { return s.repo.ListPosUsers(tenantID, locationID, includeInactive) } func (s *posService) DeactivateUser(tenantID, locationID, userID int) error { return s.repo.DeactivatePosUser(tenantID, locationID, userID) } // LoginWithPin mints a fresh session for whoever the PIN belongs to. // // A new token rather than a reused one, because the token carries the role and // a cashier taking over from a supervisor must not inherit their permissions. func (s *posService) LoginWithPin(tenantID, locationID int, pin string) (*models.PosSession, error) { session, err := s.repo.PosLoginByPin(tenantID, locationID, pin) if err != nil { return nil, err } return s.mint(session, "") } func (s *posService) ConfigidFor(tenantID int) int { return s.repo.PosConfigidFor(tenantID) }