package services import ( "errors" "testing" "nearle/models" "nearle/repositories" ) /* What these defend. A receipt exists for one reason: the ingest service's batch id is the ONLY credential for reading a result back, it is handed out exactly once, and an unreviewed drop is deleted after seven days. So the failure that matters is not a wrong count — it is a receipt that gets stored without the id, or a receipt whose status is reported as something more finished than it is. `pending` in particular is not a formality. Nothing runs on arrival at the ingest service; a drop waits for an admin to press Start. A receipt that defaulted to `queued` would claim the pipeline had the file when it is in fact waiting for a person, and the counts beside it — all zero, because nothing has run — would read as an import that found nothing. */ // fakeUploadRepo records what reached the repository. Embedded rather than // implemented: a method these tests do not exercise is nil and panics loudly if // the service starts calling it, which is the failure worth having. type fakeUploadRepo struct { repositories.CatalogueUploadRepository recorded []models.CatalogueUpload updated []models.CatalogueUpload shelved []struct { batchID string shelved, skipped int } err error } func (f *fakeUploadRepo) Record(upload *models.CatalogueUpload) error { if f.err != nil { return f.err } f.recorded = append(f.recorded, *upload) return nil } func (f *fakeUploadRepo) UpdateStatus(upload *models.CatalogueUpload) error { if f.err != nil { return f.err } f.updated = append(f.updated, *upload) return nil } func (f *fakeUploadRepo) MarkShelved(batchID string, shelved, skipped int) error { if f.err != nil { return f.err } f.shelved = append(f.shelved, struct { batchID string shelved, skipped int }{batchID, shelved, skipped}) return nil } func (f *fakeUploadRepo) List(tenantID, locationID, pageNo, pageSize int) ([]models.CatalogueUpload, error) { return nil, f.err } // A drop that has only just been accepted has not run, and the receipt has to // say so. `queued` would be wrong in the specific way that matters: it means // "approved, waiting for the worker", and this file is waiting for a person. func TestRecordDefaultsToPendingNotQueued(t *testing.T) { repo := &fakeUploadRepo{} svc := NewCatalogueUploadService(repo) upload := &models.CatalogueUpload{Batchid: "abc123", Tenantid: 1135} if err := svc.Record(upload); err != nil { t.Fatalf("Record: %v", err) } if len(repo.recorded) != 1 { t.Fatalf("want 1 recorded receipt, got %d", len(repo.recorded)) } if got := repo.recorded[0].Laststatus; got != "pending" { t.Errorf("a just-accepted drop should read pending, got %q", got) } } // A caller who already knows the status keeps it. The default is a fallback for // a fresh drop, not an override — a receipt being re-recorded from a poll that // already saw `running` must not be walked backwards. func TestRecordKeepsAnExplicitStatus(t *testing.T) { repo := &fakeUploadRepo{} svc := NewCatalogueUploadService(repo) if err := svc.Record(&models.CatalogueUpload{ Batchid: "abc123", Tenantid: 1135, Laststatus: "running", }); err != nil { t.Fatalf("Record: %v", err) } if got := repo.recorded[0].Laststatus; got != "running" { t.Errorf("want the caller's own status preserved, got %q", got) } } // A repository failure is the caller's problem, not something to swallow. The // receipt is the only durable copy of the batch id, so a write that did not // happen must never look like one that did. func TestRecordSurfacesRepositoryFailure(t *testing.T) { boom := errors.New("unique violation") svc := NewCatalogueUploadService(&fakeUploadRepo{err: boom}) err := svc.Record(&models.CatalogueUpload{Batchid: "abc123", Tenantid: 1135}) if !errors.Is(err, boom) { t.Fatalf("want the repository error surfaced, got %v", err) } } // Zero is a real answer. A run that inserted nothing because every product was // already present is a complete success, and the counts have to be storable as // zero rather than treated as "not supplied". func TestUpdateStatusCarriesZeroCounts(t *testing.T) { repo := &fakeUploadRepo{} svc := NewCatalogueUploadService(repo) if err := svc.UpdateStatus(&models.CatalogueUpload{ Batchid: "abc123", Laststatus: "done", Inserted: 0, Skipped: 12, }); err != nil { t.Fatalf("UpdateStatus: %v", err) } if len(repo.updated) != 1 { t.Fatalf("want 1 update, got %d", len(repo.updated)) } if repo.updated[0].Inserted != 0 || repo.updated[0].Skipped != 12 { t.Errorf("counts mangled: %+v", repo.updated[0]) } } // Shelving is a separate claim from ingestion and travels separately. The // ingest service confirms the shared global catalogue, which holds no price and // no stock; only this records that a shop can actually sell the products. func TestMarkShelvedPassesBothCounts(t *testing.T) { repo := &fakeUploadRepo{} svc := NewCatalogueUploadService(repo) if err := svc.MarkShelved("abc123", 18, 2); err != nil { t.Fatalf("MarkShelved: %v", err) } if len(repo.shelved) != 1 { t.Fatalf("want 1 shelving record, got %d", len(repo.shelved)) } got := repo.shelved[0] if got.batchID != "abc123" || got.shelved != 18 || got.skipped != 2 { t.Errorf("want abc123/18/2, got %v", got) } }