changes on agent

This commit is contained in:
2026-08-31 12:33:10 +05:30
parent 76554d26e5
commit ffc66462fc
9 changed files with 715 additions and 25 deletions

View File

@@ -0,0 +1,150 @@
package repositories
import (
"errors"
"strings"
"nearle/models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type CatalogueUploadRepository interface {
Record(upload *models.CatalogueUpload) error
List(tenantID, locationID, pageNo, pageSize int) ([]models.CatalogueUpload, error)
UpdateStatus(upload *models.CatalogueUpload) error
MarkShelved(batchID string, shelved, skipped int) error
}
type catalogueUploadRepository struct {
db *gorm.DB
}
func NewCatalogueUploadRepository(db *gorm.DB) CatalogueUploadRepository {
return &catalogueUploadRepository{db: db}
}
// Record stores the receipt, or returns the one already stored.
//
// Idempotent on `batchid`, which matters more than it looks. The console writes
// this the instant the drop is accepted and then polls; a refresh, a retry after
// a dropped response, or two tabs watching the same upload would otherwise each
// insert a row, and the operator would see one upload three times.
//
// `DoNothing` rather than an update, because everything a second caller could
// supply here is the same data it supplied the first time. Progress arrives
// through UpdateStatus, which is the call that is allowed to overwrite.
func (r *catalogueUploadRepository) Record(upload *models.CatalogueUpload) error {
if strings.TrimSpace(upload.Batchid) == "" {
// Without the id the row is worthless: the id is the ONLY credential
// for reading the result back, and nothing else in the receipt can
// stand in for it.
return errors.New("batchid is required — it is the only way to read the result back")
}
if err := r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "batchid"}},
DoNothing: true,
}).Create(upload).Error; err != nil {
return err
}
// A conflict leaves Uploadid zero and the caller with nothing to show, so
// the existing row is read back and returned as if it had just been made.
if upload.Uploadid == 0 {
return r.db.Where("batchid = ?", upload.Batchid).First(upload).Error
}
return nil
}
// List returns receipts newest first, scoped to whoever is asking.
//
// A tenant of 0 means "every tenant" and is how a Nearle Admin sees the whole
// platform; a store login always sends its own. The scoping is deliberately the
// same shape as the import itself — see the note on ImportScope in the console —
// because a receipt that could be read by the wrong shop would leak what another
// merchant stocks.
//
// The two names are LEFT joined. A tenant or branch can be deleted after the
// upload, and a receipt that vanishes with it would take the audit trail with
// it; joined this way the row survives, just unnamed.
func (r *catalogueUploadRepository) List(tenantID, locationID, pageNo, pageSize int) ([]models.CatalogueUpload, error) {
var uploads []models.CatalogueUpload
query := r.db.Table("catalogueuploads").
Select("catalogueuploads.*, tenants.tenantname, tenantlocations.locationname").
Joins("left join tenants on tenants.tenantid = catalogueuploads.tenantid").
Joins("left join tenantlocations on tenantlocations.locationid = catalogueuploads.locationid")
if tenantID > 0 {
query = query.Where("catalogueuploads.tenantid = ?", tenantID)
}
if locationID > 0 {
query = query.Where("catalogueuploads.locationid = ?", locationID)
}
if pageNo < 1 {
pageNo = 1
}
if pageSize < 1 || pageSize > 200 {
pageSize = 50
}
err := query.
Order("catalogueuploads.created DESC").
Offset((pageNo - 1) * pageSize).
Limit(pageSize).
Find(&uploads).Error
return uploads, err
}
// UpdateStatus caches what the ingest service last said about a drop.
//
// Only the fields the service owns are written, and they are written by column
// rather than by struct: a zero-valued struct field would otherwise be skipped
// by GORM's non-zero update, and "0 inserted" is a real answer that has to be
// storable. Everything the console owns — tenant, branch, filename, who sent it
// — is deliberately not touched, so a stale browser cannot rewrite the identity
// of a receipt while reporting progress on it.
func (r *catalogueUploadRepository) UpdateStatus(upload *models.CatalogueUpload) error {
if strings.TrimSpace(upload.Batchid) == "" {
return errors.New("batchid is required")
}
fields := map[string]any{
"laststatus": upload.Laststatus,
"inserted": upload.Inserted,
"backfilled": upload.Backfilled,
"skipped": upload.Skipped,
"rejected": upload.Rejected,
}
// Never blanked. A drop reports `released_to` once and then the drop itself
// is spent; a later poll of the RUN carries no run id of its own, and
// writing that empty value back would lose the only pointer from the id we
// hold to the results.
if strings.TrimSpace(upload.Runid) != "" {
fields["runid"] = upload.Runid
}
return r.db.Model(&models.CatalogueUpload{}).
Where("batchid = ?", upload.Batchid).
Updates(fields).Error
}
// MarkShelved records the half of the story the ingest service knows nothing
// about: that the products were priced, put on a branch's shelf and given their
// opening stock.
//
// Kept apart from UpdateStatus because the two answer different questions and
// fail independently. "The catalogue has them" and "this shop can sell them"
// are not the same claim, and a receipt that showed only the first would report
// success for products no customer can buy.
func (r *catalogueUploadRepository) MarkShelved(batchID string, shelved, skipped int) error {
if strings.TrimSpace(batchID) == "" {
return errors.New("batchid is required")
}
return r.db.Model(&models.CatalogueUpload{}).
Where("batchid = ?", batchID).
Updates(map[string]any{
"shelvedcount": shelved,
"skippedcount": skipped,
"shelvedat": gorm.Expr("CURRENT_TIMESTAMP"),
}).Error
}