405 lines
14 KiB
Go
405 lines
14 KiB
Go
package countryregion
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"hyapp-admin-server/internal/appctx"
|
|
"hyapp-admin-server/internal/integration/roomclient"
|
|
"hyapp-admin-server/internal/model"
|
|
"hyapp-admin-server/internal/repository"
|
|
)
|
|
|
|
const CountryCodeRenameJobType = "country-code-rename"
|
|
|
|
var errCountryRenameJobConflict = errors.New("country code rename job already exists")
|
|
|
|
type countryCodeRenameJobStore interface {
|
|
CreateJob(job *model.AdminJob) error
|
|
ListActiveJobsByType(jobType string) ([]model.AdminJob, error)
|
|
RenameCountryCodeAdminReferences(appCode string, oldCountryCode string, newCountryCode string, nowMS int64) (repository.CountryCodeAdminReferenceCounts, error)
|
|
}
|
|
|
|
type countryCodeRoomClient interface {
|
|
RenameOwnerCountryCode(ctx context.Context, req roomclient.RenameOwnerCountryCodeRequest) (roomclient.RenameOwnerCountryCodeResult, error)
|
|
}
|
|
|
|
type countryCodeRenameResponse struct {
|
|
JobID uint `json:"jobId"`
|
|
Status string `json:"status"`
|
|
CountryID int64 `json:"countryId"`
|
|
OldCountryCode string `json:"oldCountryCode"`
|
|
NewCountryCode string `json:"newCountryCode"`
|
|
}
|
|
|
|
type countryCodeRenameJobPayload struct {
|
|
AppCode string `json:"appCode"`
|
|
CountryID int64 `json:"countryId"`
|
|
OldCountryCode string `json:"oldCountryCode"`
|
|
NewCountryCode string `json:"newCountryCode"`
|
|
CountryName string `json:"countryName"`
|
|
CountryDisplayName string `json:"countryDisplayName"`
|
|
SortOrder int32 `json:"sortOrder"`
|
|
ISOAlpha3 string `json:"isoAlpha3"`
|
|
ISONumeric string `json:"isoNumeric"`
|
|
PhoneCountryCode string `json:"phoneCountryCode"`
|
|
Flag string `json:"flag"`
|
|
ActorID int64 `json:"actorId"`
|
|
ActorName string `json:"actorName"`
|
|
RequestID string `json:"requestId"`
|
|
}
|
|
|
|
type countryCodeRenameResult struct {
|
|
CountryID int64 `json:"countryId"`
|
|
OldCountryCode string `json:"oldCountryCode"`
|
|
NewCountryCode string `json:"newCountryCode"`
|
|
Users int64 `json:"users"`
|
|
RegionMappings int64 `json:"regionMappings"`
|
|
RebuildTasks int64 `json:"rebuildTasks"`
|
|
Banners int64 `json:"banners"`
|
|
SplashScreens int64 `json:"splashScreens"`
|
|
RoomListEntries int64 `json:"roomListEntries"`
|
|
RoomSnapshots int64 `json:"roomSnapshots"`
|
|
CompletedAtMs int64 `json:"completedAtMs"`
|
|
}
|
|
|
|
type countryCodeRenameUserCounts struct {
|
|
Users int64
|
|
RegionMappings int64
|
|
RebuildTasks int64
|
|
}
|
|
|
|
func (s *Service) RenameCountryCode(ctx context.Context, actorID int64, actorName string, countryID int64, requestID string, req renameCountryCodeRequest) (*countryCodeRenameResponse, error) {
|
|
if s.userDB == nil {
|
|
return nil, errors.New("user db is not configured")
|
|
}
|
|
if s.jobStore == nil {
|
|
return nil, errors.New("job store is not configured")
|
|
}
|
|
req = normalizeRenameCountryCodeRequest(req)
|
|
if countryID <= 0 || strings.TrimSpace(requestID) == "" || !validCountryRenameRequest(req) {
|
|
return nil, errCountryInvalid
|
|
}
|
|
|
|
tx, err := s.userDB.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
country, err := queryCountryForUpdate(ctx, tx, countryID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
oldCountryCode := strings.ToUpper(strings.TrimSpace(country.CountryCode))
|
|
if oldCountryCode == req.CountryCode {
|
|
return nil, errCountryInvalid
|
|
}
|
|
if err := ensureCountryCodeAvailable(ctx, tx, countryID, req.CountryCode); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.ensureNoActiveCountryCodeRenameJob(ctx, appctx.FromContext(ctx), countryID, oldCountryCode, req.CountryCode); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payload := countryCodeRenameJobPayload{
|
|
AppCode: appctx.FromContext(ctx),
|
|
CountryID: countryID,
|
|
OldCountryCode: oldCountryCode,
|
|
NewCountryCode: req.CountryCode,
|
|
CountryName: req.CountryName,
|
|
CountryDisplayName: req.CountryDisplayName,
|
|
SortOrder: req.SortOrder,
|
|
ISOAlpha3: req.ISOAlpha3,
|
|
ISONumeric: req.ISONumeric,
|
|
PhoneCountryCode: req.PhoneCountryCode,
|
|
Flag: req.Flag,
|
|
ActorID: actorID,
|
|
ActorName: strings.TrimSpace(actorName),
|
|
RequestID: strings.TrimSpace(requestID),
|
|
}
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
job := model.AdminJob{
|
|
Type: CountryCodeRenameJobType,
|
|
Status: model.JobStatusPending,
|
|
PayloadJSON: string(body),
|
|
CreatedBy: uint(actorID),
|
|
CreatedByName: strings.TrimSpace(actorName),
|
|
}
|
|
if err := s.jobStore.CreateJob(&job); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &countryCodeRenameResponse{
|
|
JobID: job.ID,
|
|
Status: job.Status,
|
|
CountryID: countryID,
|
|
OldCountryCode: oldCountryCode,
|
|
NewCountryCode: req.CountryCode,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) HandleCountryCodeRenameJob(ctx context.Context, job *model.AdminJob) (string, error) {
|
|
if job == nil {
|
|
return "", errors.New("job is required")
|
|
}
|
|
if s.userDB == nil {
|
|
return "", errors.New("user db is not configured")
|
|
}
|
|
if s.jobStore == nil {
|
|
return "", errors.New("job store is not configured")
|
|
}
|
|
if s.roomClient == nil {
|
|
return "", errors.New("room service client is not configured")
|
|
}
|
|
var payload countryCodeRenameJobPayload
|
|
if err := json.Unmarshal([]byte(job.PayloadJSON), &payload); err != nil {
|
|
return "", err
|
|
}
|
|
payload = normalizeCountryCodeRenamePayload(payload)
|
|
if !validCountryCodeRenamePayload(payload) {
|
|
return "", errCountryInvalid
|
|
}
|
|
|
|
ctx = appctx.WithContext(ctx, payload.AppCode)
|
|
userCounts, err := s.renameCountryCodeInUserDB(ctx, payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
nowMS := time.Now().UTC().UnixMilli()
|
|
adminCounts, err := s.jobStore.RenameCountryCodeAdminReferences(payload.AppCode, payload.OldCountryCode, payload.NewCountryCode, nowMS)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
roomCounts, err := s.roomClient.RenameOwnerCountryCode(ctx, roomclient.RenameOwnerCountryCodeRequest{
|
|
RequestID: firstNonEmptyString(payload.RequestID, fmt.Sprintf("country-code-rename-job-%d", job.ID)),
|
|
OldCountryCode: payload.OldCountryCode,
|
|
NewCountryCode: payload.NewCountryCode,
|
|
AdminID: uint64(payload.ActorID),
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
result := countryCodeRenameResult{
|
|
CountryID: payload.CountryID,
|
|
OldCountryCode: payload.OldCountryCode,
|
|
NewCountryCode: payload.NewCountryCode,
|
|
Users: userCounts.Users,
|
|
RegionMappings: userCounts.RegionMappings,
|
|
RebuildTasks: userCounts.RebuildTasks,
|
|
Banners: adminCounts.Banners,
|
|
SplashScreens: adminCounts.SplashScreens,
|
|
RoomListEntries: roomCounts.RoomListEntries,
|
|
RoomSnapshots: roomCounts.RoomSnapshots,
|
|
CompletedAtMs: time.Now().UTC().UnixMilli(),
|
|
}
|
|
body, err := json.Marshal(result)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(body), nil
|
|
}
|
|
|
|
func (s *Service) renameCountryCodeInUserDB(ctx context.Context, payload countryCodeRenameJobPayload) (countryCodeRenameUserCounts, error) {
|
|
tx, err := s.userDB.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return countryCodeRenameUserCounts{}, err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
country, err := queryCountryForUpdate(ctx, tx, payload.CountryID)
|
|
if err != nil {
|
|
return countryCodeRenameUserCounts{}, err
|
|
}
|
|
currentCode := strings.ToUpper(strings.TrimSpace(country.CountryCode))
|
|
if currentCode != payload.OldCountryCode && currentCode != payload.NewCountryCode {
|
|
return countryCodeRenameUserCounts{}, errCountryConflict
|
|
}
|
|
if err := ensureCountryCodeAvailable(ctx, tx, payload.CountryID, payload.NewCountryCode); err != nil {
|
|
return countryCodeRenameUserCounts{}, err
|
|
}
|
|
|
|
nowMS := time.Now().UTC().UnixMilli()
|
|
if _, err := tx.ExecContext(ctx, `
|
|
UPDATE countries
|
|
SET country_code = ?, country_name = ?, country_display_name = ?,
|
|
iso_alpha3 = ?, iso_numeric = ?, phone_country_code = ?, flag = ?, sort_order = ?,
|
|
updated_by_user_id = ?, updated_at_ms = ?
|
|
WHERE app_code = ? AND country_id = ?
|
|
`, payload.NewCountryCode, payload.CountryName, payload.CountryDisplayName, nullableString(payload.ISOAlpha3), nullableString(payload.ISONumeric), nullableString(payload.PhoneCountryCode), payload.Flag, payload.SortOrder, nullableInt64(payload.ActorID), nowMS, payload.AppCode, payload.CountryID); err != nil {
|
|
return countryCodeRenameUserCounts{}, mapCountryWriteError(err)
|
|
}
|
|
|
|
regionResult, err := tx.ExecContext(ctx, `
|
|
UPDATE region_countries
|
|
SET country_code = ?, updated_by_user_id = ?, updated_at_ms = ?
|
|
WHERE app_code = ? AND country_code = ?
|
|
`, payload.NewCountryCode, nullableInt64(payload.ActorID), nowMS, payload.AppCode, payload.OldCountryCode)
|
|
if err != nil {
|
|
return countryCodeRenameUserCounts{}, err
|
|
}
|
|
userResult, err := tx.ExecContext(ctx, `
|
|
UPDATE users
|
|
SET country = ?, updated_at_ms = ?
|
|
WHERE app_code = ? AND country = ?
|
|
`, payload.NewCountryCode, nowMS, payload.AppCode, payload.OldCountryCode)
|
|
if err != nil {
|
|
return countryCodeRenameUserCounts{}, err
|
|
}
|
|
taskResult, err := tx.ExecContext(ctx, `
|
|
UPDATE user_region_rebuild_tasks
|
|
SET country_code = ?, updated_at_ms = ?
|
|
WHERE app_code = ? AND country_code = ? AND status IN (?, ?)
|
|
`, payload.NewCountryCode, nowMS, payload.AppCode, payload.OldCountryCode, regionRebuildTaskStatusPending, regionRebuildTaskStatusRunning)
|
|
if err != nil {
|
|
return countryCodeRenameUserCounts{}, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return countryCodeRenameUserCounts{}, err
|
|
}
|
|
|
|
regionRows, _ := regionResult.RowsAffected()
|
|
userRows, _ := userResult.RowsAffected()
|
|
taskRows, _ := taskResult.RowsAffected()
|
|
return countryCodeRenameUserCounts{Users: userRows, RegionMappings: regionRows, RebuildTasks: taskRows}, nil
|
|
}
|
|
|
|
func queryCountryForUpdate(ctx context.Context, tx *sql.Tx, countryID int64) (*userclientCountryForRename, error) {
|
|
var country userclientCountryForRename
|
|
if err := tx.QueryRowContext(ctx, `
|
|
SELECT country_id, country_code
|
|
FROM countries
|
|
WHERE app_code = ? AND country_id = ?
|
|
FOR UPDATE
|
|
`, appctx.FromContext(ctx), countryID).Scan(&country.CountryID, &country.CountryCode); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, errCountryNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return &country, nil
|
|
}
|
|
|
|
type userclientCountryForRename struct {
|
|
CountryID int64
|
|
CountryCode string
|
|
}
|
|
|
|
func ensureCountryCodeAvailable(ctx context.Context, tx *sql.Tx, countryID int64, countryCode string) error {
|
|
var existingID int64
|
|
err := tx.QueryRowContext(ctx, `
|
|
SELECT country_id
|
|
FROM countries
|
|
WHERE app_code = ? AND country_code = ? AND country_id <> ?
|
|
LIMIT 1
|
|
FOR UPDATE
|
|
`, appctx.FromContext(ctx), countryCode, countryID).Scan(&existingID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return errCountryConflict
|
|
}
|
|
|
|
func (s *Service) ensureNoActiveCountryCodeRenameJob(ctx context.Context, appCode string, countryID int64, oldCountryCode string, newCountryCode string) error {
|
|
jobs, err := s.jobStore.ListActiveJobsByType(CountryCodeRenameJobType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, job := range jobs {
|
|
var payload countryCodeRenameJobPayload
|
|
if err := json.Unmarshal([]byte(job.PayloadJSON), &payload); err != nil {
|
|
continue
|
|
}
|
|
payload = normalizeCountryCodeRenamePayload(payload)
|
|
if payload.AppCode != appctx.Normalize(appCode) {
|
|
continue
|
|
}
|
|
if payload.CountryID == countryID ||
|
|
payload.OldCountryCode == oldCountryCode ||
|
|
payload.OldCountryCode == newCountryCode ||
|
|
payload.NewCountryCode == oldCountryCode ||
|
|
payload.NewCountryCode == newCountryCode {
|
|
return errCountryRenameJobConflict
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeRenameCountryCodeRequest(req renameCountryCodeRequest) renameCountryCodeRequest {
|
|
req.CountryName = strings.TrimSpace(req.CountryName)
|
|
req.CountryCode = strings.ToUpper(strings.TrimSpace(req.CountryCode))
|
|
req.CountryDisplayName = strings.TrimSpace(req.CountryDisplayName)
|
|
req.ISOAlpha3 = strings.ToUpper(strings.TrimSpace(req.ISOAlpha3))
|
|
req.ISONumeric = strings.TrimSpace(req.ISONumeric)
|
|
req.PhoneCountryCode = strings.TrimSpace(req.PhoneCountryCode)
|
|
req.Flag = strings.TrimSpace(req.Flag)
|
|
return req
|
|
}
|
|
|
|
func validCountryRenameRequest(req renameCountryCodeRequest) bool {
|
|
return validCountryCreateRequest(createCountryRequest{
|
|
CountryName: req.CountryName,
|
|
CountryCode: req.CountryCode,
|
|
CountryDisplayName: req.CountryDisplayName,
|
|
SortOrder: req.SortOrder,
|
|
ISOAlpha3: req.ISOAlpha3,
|
|
ISONumeric: req.ISONumeric,
|
|
PhoneCountryCode: req.PhoneCountryCode,
|
|
Flag: req.Flag,
|
|
})
|
|
}
|
|
|
|
func normalizeCountryCodeRenamePayload(payload countryCodeRenameJobPayload) countryCodeRenameJobPayload {
|
|
payload.AppCode = appctx.Normalize(payload.AppCode)
|
|
payload.OldCountryCode = strings.ToUpper(strings.TrimSpace(payload.OldCountryCode))
|
|
payload.NewCountryCode = strings.ToUpper(strings.TrimSpace(payload.NewCountryCode))
|
|
payload.CountryName = strings.TrimSpace(payload.CountryName)
|
|
payload.CountryDisplayName = strings.TrimSpace(payload.CountryDisplayName)
|
|
payload.ISOAlpha3 = strings.ToUpper(strings.TrimSpace(payload.ISOAlpha3))
|
|
payload.ISONumeric = strings.TrimSpace(payload.ISONumeric)
|
|
payload.PhoneCountryCode = strings.TrimSpace(payload.PhoneCountryCode)
|
|
payload.Flag = strings.TrimSpace(payload.Flag)
|
|
payload.ActorName = strings.TrimSpace(payload.ActorName)
|
|
payload.RequestID = strings.TrimSpace(payload.RequestID)
|
|
return payload
|
|
}
|
|
|
|
func validCountryCodeRenamePayload(payload countryCodeRenameJobPayload) bool {
|
|
return payload.CountryID > 0 &&
|
|
payload.ActorID >= 0 &&
|
|
payload.OldCountryCode != payload.NewCountryCode &&
|
|
countryCodePattern.MatchString(payload.OldCountryCode) &&
|
|
validCountryRenameRequest(renameCountryCodeRequest{
|
|
CountryName: payload.CountryName,
|
|
CountryCode: payload.NewCountryCode,
|
|
CountryDisplayName: payload.CountryDisplayName,
|
|
SortOrder: payload.SortOrder,
|
|
ISOAlpha3: payload.ISOAlpha3,
|
|
ISONumeric: payload.ISONumeric,
|
|
PhoneCountryCode: payload.PhoneCountryCode,
|
|
Flag: payload.Flag,
|
|
})
|
|
}
|
|
|
|
func firstNonEmptyString(values ...string) string {
|
|
for _, value := range values {
|
|
if strings.TrimSpace(value) != "" {
|
|
return strings.TrimSpace(value)
|
|
}
|
|
}
|
|
return ""
|
|
}
|