diff --git a/internal/services/sync/service.go b/internal/services/sync/service.go index d6c7588..b69215c 100644 --- a/internal/services/sync/service.go +++ b/internal/services/sync/service.go @@ -685,17 +685,36 @@ func (s *Service) pushConfigurationUpdate(change *localdb.PendingChange) error { } if localCfg.ServerID == nil { - // Configuration hasn't been synced yet, try to find it on server by UUID - serverCfg, err := configRepo.GetByUUID(cfg.UUID) - if err != nil { - return fmt.Errorf("configuration not yet synced to server: %w", err) + // Configuration hasn't been synced yet, try to find it on server by UUID. + // If not found (e.g. stale create was skipped), create it from current snapshot. + serverCfg, getErr := configRepo.GetByUUID(cfg.UUID) + if getErr != nil { + if !errors.Is(getErr, gorm.ErrRecordNotFound) { + return fmt.Errorf("loading configuration from server: %w", getErr) + } + if createErr := configRepo.Create(&cfg); createErr != nil { + // Idempotency fallback: configuration may have been created concurrently. + existing, existingErr := configRepo.GetByUUID(cfg.UUID) + if existingErr != nil { + return fmt.Errorf("creating missing configuration on server: %w", createErr) + } + cfg.ID = existing.ID + } + if cfg.ID == 0 { + existing, existingErr := configRepo.GetByUUID(cfg.UUID) + if existingErr != nil { + return fmt.Errorf("loading created configuration from server: %w", existingErr) + } + cfg.ID = existing.ID + } + } else { + cfg.ID = serverCfg.ID } - cfg.ID = serverCfg.ID - // Update local with server ID - serverID := serverCfg.ID - localCfg.ServerID = &serverID - s.localDB.SaveConfiguration(localCfg) + // Update local with server ID + serverID := cfg.ID + localCfg.ServerID = &serverID + s.localDB.SaveConfiguration(localCfg) } else { cfg.ID = *localCfg.ServerID } diff --git a/internal/services/sync/service_projects_push_test.go b/internal/services/sync/service_projects_push_test.go index 1c1fe5a..bad7e91 100644 --- a/internal/services/sync/service_projects_push_test.go +++ b/internal/services/sync/service_projects_push_test.go @@ -202,6 +202,57 @@ func TestPushPendingChangesCreateIsIdempotent(t *testing.T) { } } +func TestPushPendingChangesCreateThenUpdateBeforeFirstPush(t *testing.T) { + local := newLocalDBForSyncTest(t) + serverDB := newServerDBForSyncTest(t) + + localSync := syncsvc.NewService(nil, local) + configService := services.NewLocalConfigurationService(local, localSync, &services.QuoteService{}, func() bool { return false }) + pushService := syncsvc.NewServiceWithDB(serverDB, local) + + created, err := configService.Create("tester", &services.CreateConfigRequest{ + Name: "Cfg v1", + Items: models.ConfigItems{{LotName: "CPU_X", Quantity: 1, UnitPrice: 700}}, + ServerCount: 1, + }) + if err != nil { + t.Fatalf("create config: %v", err) + } + + if _, err := configService.UpdateNoAuth(created.UUID, &services.CreateConfigRequest{ + Name: "Cfg v2", + Items: models.ConfigItems{{LotName: "CPU_X", Quantity: 3, UnitPrice: 700}}, + ServerCount: 1, + ProjectUUID: created.ProjectUUID, + }); err != nil { + t.Fatalf("update config before first push: %v", err) + } + + pushed, err := pushService.PushPendingChanges() + if err != nil { + t.Fatalf("push pending changes: %v", err) + } + if pushed < 1 { + t.Fatalf("expected at least one pushed change, got %d", pushed) + } + + var serverCfg models.Configuration + if err := serverDB.Where("uuid = ?", created.UUID).First(&serverCfg).Error; err != nil { + t.Fatalf("configuration not pushed to server: %v", err) + } + if serverCfg.Name != "Cfg v2" { + t.Fatalf("expected latest update to be pushed, got %q", serverCfg.Name) + } + + localCfg, err := local.GetConfigurationByUUID(created.UUID) + if err != nil { + t.Fatalf("get local config: %v", err) + } + if localCfg.ServerID == nil || *localCfg.ServerID == 0 { + t.Fatalf("expected local configuration to have server_id after push, got %+v", localCfg.ServerID) + } +} + func newLocalDBForSyncTest(t *testing.T) *localdb.LocalDB { t.Helper() localPath := filepath.Join(t.TempDir(), "local.db")