Add vendor workspace import and pricing export workflow
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -213,17 +214,64 @@ func ensureClientMigrationRegistryTable(db *gorm.DB) error {
|
||||
if err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS qt_client_schema_state (
|
||||
username VARCHAR(100) NOT NULL,
|
||||
hostname VARCHAR(255) NOT NULL DEFAULT '',
|
||||
last_applied_migration_id VARCHAR(128) NULL,
|
||||
app_version VARCHAR(64) NULL,
|
||||
last_sync_at DATETIME NULL,
|
||||
last_sync_status VARCHAR(32) NULL,
|
||||
pending_changes_count INT NOT NULL DEFAULT 0,
|
||||
pending_errors_count INT NOT NULL DEFAULT 0,
|
||||
configurations_count INT NOT NULL DEFAULT 0,
|
||||
projects_count INT NOT NULL DEFAULT 0,
|
||||
estimate_pricelist_version VARCHAR(128) NULL,
|
||||
warehouse_pricelist_version VARCHAR(128) NULL,
|
||||
competitor_pricelist_version VARCHAR(128) NULL,
|
||||
last_sync_error_code VARCHAR(128) NULL,
|
||||
last_sync_error_text TEXT NULL,
|
||||
last_checked_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (username),
|
||||
PRIMARY KEY (username, hostname),
|
||||
INDEX idx_qt_client_schema_state_checked (last_checked_at)
|
||||
)
|
||||
`).Error; err != nil {
|
||||
return fmt.Errorf("create qt_client_schema_state table: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tableExists(db, "qt_client_schema_state") {
|
||||
if err := db.Exec(`
|
||||
ALTER TABLE qt_client_schema_state
|
||||
ADD COLUMN IF NOT EXISTS hostname VARCHAR(255) NOT NULL DEFAULT '' AFTER username
|
||||
`).Error; err != nil {
|
||||
return fmt.Errorf("add qt_client_schema_state.hostname: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Exec(`
|
||||
ALTER TABLE qt_client_schema_state
|
||||
DROP PRIMARY KEY,
|
||||
ADD PRIMARY KEY (username, hostname)
|
||||
`).Error; err != nil && !isDuplicatePrimaryKeyDefinition(err) {
|
||||
return fmt.Errorf("set qt_client_schema_state primary key: %w", err)
|
||||
}
|
||||
|
||||
for _, stmt := range []string{
|
||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS last_sync_at DATETIME NULL AFTER app_version",
|
||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS last_sync_status VARCHAR(32) NULL AFTER last_sync_at",
|
||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS pending_changes_count INT NOT NULL DEFAULT 0 AFTER last_sync_status",
|
||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS pending_errors_count INT NOT NULL DEFAULT 0 AFTER pending_changes_count",
|
||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS configurations_count INT NOT NULL DEFAULT 0 AFTER pending_errors_count",
|
||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS projects_count INT NOT NULL DEFAULT 0 AFTER configurations_count",
|
||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS estimate_pricelist_version VARCHAR(128) NULL AFTER projects_count",
|
||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS warehouse_pricelist_version VARCHAR(128) NULL AFTER estimate_pricelist_version",
|
||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS competitor_pricelist_version VARCHAR(128) NULL AFTER warehouse_pricelist_version",
|
||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS last_sync_error_code VARCHAR(128) NULL AFTER competitor_pricelist_version",
|
||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS last_sync_error_text TEXT NULL AFTER last_sync_error_code",
|
||||
} {
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
return fmt.Errorf("expand qt_client_schema_state: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -351,19 +399,108 @@ func (s *Service) reportClientSchemaState(mariaDB *gorm.DB, checkedAt time.Time)
|
||||
if username == "" {
|
||||
return nil
|
||||
}
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
hostname = ""
|
||||
}
|
||||
hostname = strings.TrimSpace(hostname)
|
||||
lastMigrationID := ""
|
||||
if id, err := s.localDB.GetLatestAppliedRemoteMigrationID(); err == nil {
|
||||
lastMigrationID = id
|
||||
}
|
||||
lastSyncAt := s.localDB.GetLastSyncTime()
|
||||
lastSyncStatus := ReadinessReady
|
||||
pendingChangesCount := s.localDB.CountPendingChanges()
|
||||
pendingErrorsCount := s.localDB.CountErroredChanges()
|
||||
configurationsCount := s.localDB.CountConfigurations()
|
||||
projectsCount := s.localDB.CountProjects()
|
||||
estimateVersion := latestPricelistVersion(s.localDB, "estimate")
|
||||
warehouseVersion := latestPricelistVersion(s.localDB, "warehouse")
|
||||
competitorVersion := latestPricelistVersion(s.localDB, "competitor")
|
||||
lastSyncErrorCode, lastSyncErrorText := latestSyncErrorState(s.localDB)
|
||||
return mariaDB.Exec(`
|
||||
INSERT INTO qt_client_schema_state (username, last_applied_migration_id, app_version, last_checked_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO qt_client_schema_state (
|
||||
username, hostname, last_applied_migration_id, app_version,
|
||||
last_sync_at, last_sync_status, pending_changes_count, pending_errors_count,
|
||||
configurations_count, projects_count,
|
||||
estimate_pricelist_version, warehouse_pricelist_version, competitor_pricelist_version,
|
||||
last_sync_error_code, last_sync_error_text,
|
||||
last_checked_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_applied_migration_id = VALUES(last_applied_migration_id),
|
||||
app_version = VALUES(app_version),
|
||||
last_sync_at = VALUES(last_sync_at),
|
||||
last_sync_status = VALUES(last_sync_status),
|
||||
pending_changes_count = VALUES(pending_changes_count),
|
||||
pending_errors_count = VALUES(pending_errors_count),
|
||||
configurations_count = VALUES(configurations_count),
|
||||
projects_count = VALUES(projects_count),
|
||||
estimate_pricelist_version = VALUES(estimate_pricelist_version),
|
||||
warehouse_pricelist_version = VALUES(warehouse_pricelist_version),
|
||||
competitor_pricelist_version = VALUES(competitor_pricelist_version),
|
||||
last_sync_error_code = VALUES(last_sync_error_code),
|
||||
last_sync_error_text = VALUES(last_sync_error_text),
|
||||
last_checked_at = VALUES(last_checked_at),
|
||||
updated_at = VALUES(updated_at)
|
||||
`, username, lastMigrationID, appmeta.Version(), checkedAt, checkedAt).Error
|
||||
`, username, hostname, lastMigrationID, appmeta.Version(),
|
||||
lastSyncAt, lastSyncStatus, pendingChangesCount, pendingErrorsCount,
|
||||
configurationsCount, projectsCount,
|
||||
estimateVersion, warehouseVersion, competitorVersion,
|
||||
lastSyncErrorCode, lastSyncErrorText,
|
||||
checkedAt, checkedAt).Error
|
||||
}
|
||||
|
||||
func isDuplicatePrimaryKeyDefinition(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := strings.ToLower(err.Error())
|
||||
return strings.Contains(msg, "multiple primary key defined") ||
|
||||
strings.Contains(msg, "duplicate key name 'primary'") ||
|
||||
strings.Contains(msg, "duplicate entry")
|
||||
}
|
||||
|
||||
func latestPricelistVersion(local *localdb.LocalDB, source string) *string {
|
||||
if local == nil {
|
||||
return nil
|
||||
}
|
||||
pl, err := local.GetLatestLocalPricelistBySource(source)
|
||||
if err != nil || pl == nil {
|
||||
return nil
|
||||
}
|
||||
version := strings.TrimSpace(pl.Version)
|
||||
if version == "" {
|
||||
return nil
|
||||
}
|
||||
return &version
|
||||
}
|
||||
|
||||
func latestSyncErrorState(local *localdb.LocalDB) (*string, *string) {
|
||||
if local == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if guard, err := local.GetSyncGuardState(); err == nil && guard != nil && strings.EqualFold(guard.Status, ReadinessBlocked) {
|
||||
return optionalString(strings.TrimSpace(guard.ReasonCode)), optionalString(strings.TrimSpace(guard.ReasonText))
|
||||
}
|
||||
|
||||
var pending localdb.PendingChange
|
||||
if err := local.DB().
|
||||
Where("TRIM(COALESCE(last_error, '')) <> ''").
|
||||
Order("id DESC").
|
||||
First(&pending).Error; err == nil {
|
||||
return optionalString("PENDING_CHANGE_ERROR"), optionalString(strings.TrimSpace(pending.LastError))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func optionalString(value string) *string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
v := strings.TrimSpace(value)
|
||||
return &v
|
||||
}
|
||||
|
||||
func normalizeVersionParts(v string) []int {
|
||||
|
||||
Reference in New Issue
Block a user