changes on agent
This commit is contained in:
150
repositories/catalogueUploadRepository.go
Normal file
150
repositories/catalogueUploadRepository.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user