Files
datamarket/internal/service/service.go
2026-04-07 21:21:18 +08:00

1038 lines
31 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"math"
"strconv"
"strings"
"time"
"gitea.starryskymeow.cn/B309/datamarket/internal/config"
"gitea.starryskymeow.cn/B309/datamarket/internal/repository"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
const (
defaultLimit int32 = 20
maxLimit int32 = 100
)
var (
allowedAssetStatuses = map[string]struct{}{
"待评估": {},
"已上架": {},
"可交易": {},
"已下架": {},
}
allowedOrderStatuses = map[string]struct{}{
"待确认": {},
"已报价": {},
"验证中": {},
"已成交": {},
"已取消": {},
}
)
type Service struct {
queries *repository.Queries
config *config.Config
now func() time.Time
}
func New(queries *repository.Queries, cfg *config.Config) *Service {
return &Service{
queries: queries,
config: cfg,
now: time.Now,
}
}
func (s *Service) CreateAsset(ctx context.Context, input AssetCreateInput) (AssetStatusResult, error) {
if err := validateAssetInput(input); err != nil {
return AssetStatusResult{}, err
}
derived, err := buildAssetDerivedFields(input)
if err != nil {
return AssetStatusResult{}, err
}
asset, err := s.queries.CreateDataAsset(ctx, repository.CreateDataAssetParams{
AssetName: strings.TrimSpace(input.AssetName),
AssetType: strings.TrimSpace(input.AssetType),
Domain: strings.TrimSpace(input.Domain),
ApplicationScene: textValue(input.ApplicationScene),
DataDescription: strings.TrimSpace(input.DataDescription),
DataScale: strings.TrimSpace(input.DataScale),
CollectionMethod: strings.TrimSpace(input.CollectionMethod),
LabelingStatus: textValue(input.LabelingStatus),
UpdateFrequency: textValue(input.UpdateFrequency),
PrivacyLevel: strings.TrimSpace(input.PrivacyLevel),
PermissionMode: strings.TrimSpace(input.PermissionMode),
SupportsValidation: input.SupportsValidation,
SellerExpectedPriceMin: numericValue(*input.SellerExpectedPriceMin),
SellerExpectedPriceMax: numericValue(*input.SellerExpectedPriceMax),
QualityLevel: textValue(&derived.QualityLevel),
ScarcityLevel: textValue(&derived.ScarcityLevel),
BaseValueScore: numericValue(derived.BaseValueScore),
BasePriceMin: numericValue(derived.BasePriceMin),
BasePriceMax: numericValue(derived.BasePriceMax),
AssetStatus: "已上架",
})
if err != nil {
return AssetStatusResult{}, internalError("create asset failed", err)
}
return AssetStatusResult{
AssetID: asset.ID.String(),
AssetStatus: asset.AssetStatus,
}, nil
}
func (s *Service) GetAsset(ctx context.Context, assetID string) (Asset, error) {
id, err := parseUUID(assetID)
if err != nil {
return Asset{}, err
}
asset, err := s.queries.GetDataAsset(ctx, id)
if err != nil {
return Asset{}, handleQueryError(err, "asset not found")
}
return toAsset(asset), nil
}
func (s *Service) ListAssets(ctx context.Context, input AssetListInput) (ListResult[Asset], error) {
params := buildAssetListParams(input)
total, err := s.queries.CountDataAssets(ctx, repository.CountDataAssetsParams{
Keyword: params.Keyword,
AssetType: params.AssetType,
Domain: params.Domain,
PrivacyLevel: params.PrivacyLevel,
SupportsValidation: params.SupportsValidation,
})
if err != nil {
return ListResult[Asset]{}, internalError("count assets failed", err)
}
items, err := s.queries.ListDataAssets(ctx, params)
if err != nil {
return ListResult[Asset]{}, internalError("list assets failed", err)
}
result := ListResult[Asset]{
List: make([]Asset, 0, len(items)),
Total: total,
Limit: params.Limit,
Offset: params.Offset,
}
for _, item := range items {
result.List = append(result.List, toAsset(item))
}
return result, nil
}
func (s *Service) UpdateAssetStatus(ctx context.Context, assetID string, update StatusUpdate) (AssetStatusResult, error) {
if strings.TrimSpace(update.Status) == "" {
return AssetStatusResult{}, badRequest("asset_status is required")
}
if _, ok := allowedAssetStatuses[update.Status]; !ok {
return AssetStatusResult{}, badRequest("asset_status is invalid")
}
id, err := parseUUID(assetID)
if err != nil {
return AssetStatusResult{}, err
}
asset, err := s.queries.UpdateDataAssetStatus(ctx, repository.UpdateDataAssetStatusParams{
ID: id,
AssetStatus: update.Status,
})
if err != nil {
return AssetStatusResult{}, handleQueryError(err, "asset not found")
}
return AssetStatusResult{
AssetID: asset.ID.String(),
AssetStatus: asset.AssetStatus,
}, nil
}
func (s *Service) CreatePricing(ctx context.Context, input PricingCreateInput) (Pricing, error) {
if err := validatePricingInput(input); err != nil {
return Pricing{}, err
}
assetID, err := parseUUID(input.AssetID)
if err != nil {
return Pricing{}, err
}
asset, err := s.queries.GetDataAsset(ctx, assetID)
if err != nil {
return Pricing{}, handleQueryError(err, "asset not found")
}
request, err := s.queries.CreateBuyerRequest(ctx, repository.CreateBuyerRequestParams{
AssetID: assetID,
TaskType: strings.TrimSpace(input.TaskType),
ModelType: strings.TrimSpace(input.ModelType),
BuyerBudgetMin: numericValue(*input.BuyerBudgetMin),
BuyerBudgetMax: numericValue(*input.BuyerBudgetMax),
PrivacyRequirement: textValue(input.PrivacyRequirement),
UsagePurpose: textValue(input.UsagePurpose),
RequestNote: textValue(input.RequestNote),
RequestStatus: "已分析",
})
if err != nil {
return Pricing{}, internalError("create buyer request failed", err)
}
derived, err := buildPricingDerivedFields(asset, request)
if err != nil {
return Pricing{}, err
}
pricing, err := s.queries.CreatePricingResult(ctx, repository.CreatePricingResultParams{
AssetID: asset.ID,
RequestID: request.ID,
ScenarioValueScore: numericValue(derived.ScenarioValueScore),
ScenarioPriceMin: numericValue(derived.ScenarioPriceMin),
ScenarioPriceMax: numericValue(derived.ScenarioPriceMax),
SuggestedPrice: numericValue(derived.SuggestedPrice),
SuccessProbability: numericValue(derived.SuccessProbability),
PricingReason1: textValue(&derived.PricingReason1),
PricingReason2: textValue(&derived.PricingReason2),
PricingReason3: textValue(&derived.PricingReason3),
VerificationSuggestion: textValue(&derived.VerificationSuggestion),
PricingStatus: "已生成",
})
if err != nil {
return Pricing{}, internalError("create pricing result failed", err)
}
return toPricing(pricing), nil
}
func (s *Service) GetPricing(ctx context.Context, pricingID string) (Pricing, error) {
id, err := parseUUID(pricingID)
if err != nil {
return Pricing{}, err
}
pricing, err := s.queries.GetPricingResult(ctx, id)
if err != nil {
return Pricing{}, handleQueryError(err, "pricing result not found")
}
return toPricing(pricing), nil
}
func (s *Service) CreateValidation(ctx context.Context, input ValidationCreateInput) (ValidationCreateResult, error) {
if strings.TrimSpace(input.ValidationType) == "" {
return ValidationCreateResult{}, badRequest("validation_type is required")
}
assetID, err := parseUUID(input.AssetID)
if err != nil {
return ValidationCreateResult{}, err
}
requestID, err := parseUUID(input.RequestID)
if err != nil {
return ValidationCreateResult{}, err
}
asset, err := s.queries.GetDataAsset(ctx, assetID)
if err != nil {
return ValidationCreateResult{}, handleQueryError(err, "asset not found")
}
if !asset.SupportsValidation {
return ValidationCreateResult{}, badRequest("asset does not support validation")
}
request, err := s.queries.GetBuyerRequest(ctx, requestID)
if err != nil {
return ValidationCreateResult{}, handleQueryError(err, "buyer request not found")
}
created, err := s.queries.CreateValidation(ctx, repository.CreateValidationParams{
AssetID: assetID,
RequestID: requestID,
ValidationType: textValue(&input.ValidationType),
ValidationRequested: true,
ValidationStatus: "处理中",
ValidationSignal: pgtype.Text{},
ValidationScore: pgtype.Numeric{},
RiskWarning: pgtype.Text{},
ContinueRecommendation: pgtype.Text{},
ValidationFinishedAt: pgtype.Timestamptz{},
})
if err != nil {
return ValidationCreateResult{}, internalError("create validation failed", err)
}
derived, err := buildValidationDerivedFields(asset, request)
if err != nil {
return ValidationCreateResult{}, err
}
_, err = s.queries.UpdateValidationResult(ctx, repository.UpdateValidationResultParams{
ID: created.ID,
ValidationStatus: "已完成",
ValidationSignal: textValue(&derived.ValidationSignal),
ValidationScore: numericValue(derived.ValidationScore),
RiskWarning: textValue(&derived.RiskWarning),
ContinueRecommendation: textValue(&derived.ContinueRecommendation),
ValidationFinishedAt: timestamptzValue(s.now()),
})
if err != nil {
return ValidationCreateResult{}, internalError("finalize validation failed", err)
}
return ValidationCreateResult{
ValidationID: created.ID.String(),
ValidationStatus: "处理中",
}, nil
}
func (s *Service) GetValidation(ctx context.Context, validationID string) (Validation, error) {
id, err := parseUUID(validationID)
if err != nil {
return Validation{}, err
}
validation, err := s.queries.GetValidation(ctx, id)
if err != nil {
return Validation{}, handleQueryError(err, "validation not found")
}
return toValidation(validation), nil
}
func (s *Service) ListValidations(ctx context.Context, input ValidationListInput) (ListResult[Validation], error) {
params := normalizePage(input.Limit, input.Offset)
total, err := s.queries.CountValidations(ctx)
if err != nil {
return ListResult[Validation]{}, internalError("count validations failed", err)
}
items, err := s.queries.ListValidations(ctx, repository.ListValidationsParams{
Limit: params.Limit,
Offset: params.Offset,
})
if err != nil {
return ListResult[Validation]{}, internalError("list validations failed", err)
}
result := ListResult[Validation]{
List: make([]Validation, 0, len(items)),
Total: total,
Limit: params.Limit,
Offset: params.Offset,
}
for _, item := range items {
result.List = append(result.List, toValidation(item))
}
return result, nil
}
func (s *Service) CreateOrder(ctx context.Context, input OrderCreateInput) (OrderCreateResult, error) {
if err := validateOrderInput(input); err != nil {
return OrderCreateResult{}, err
}
assetID, err := parseUUID(input.AssetID)
if err != nil {
return OrderCreateResult{}, err
}
requestID, err := parseUUID(input.RequestID)
if err != nil {
return OrderCreateResult{}, err
}
pricingID, err := parseUUID(input.PricingID)
if err != nil {
return OrderCreateResult{}, err
}
asset, err := s.queries.GetDataAsset(ctx, assetID)
if err != nil {
return OrderCreateResult{}, handleQueryError(err, "asset not found")
}
if _, err := s.queries.GetBuyerRequest(ctx, requestID); err != nil {
return OrderCreateResult{}, handleQueryError(err, "buyer request not found")
}
pricing, err := s.queries.GetPricingResult(ctx, pricingID)
if err != nil {
return OrderCreateResult{}, handleQueryError(err, "pricing result not found")
}
validationID := pgtype.UUID{}
validationUsed := false
if input.ValidationID != nil && strings.TrimSpace(*input.ValidationID) != "" {
validationID, err = parseUUID(*input.ValidationID)
if err != nil {
return OrderCreateResult{}, err
}
if _, err := s.queries.GetValidation(ctx, validationID); err != nil {
return OrderCreateResult{}, handleQueryError(err, "validation not found")
}
validationUsed = true
}
negotiationMin, negotiationMax, err := buildNegotiationRange(pricing, *input.CurrentPrice)
if err != nil {
return OrderCreateResult{}, err
}
order, err := s.queries.CreateOrder(ctx, repository.CreateOrderParams{
AssetID: asset.ID,
RequestID: requestID,
PricingID: pricingID,
ValidationID: validationID,
AssetName: asset.AssetName,
CurrentPrice: numericValue(*input.CurrentPrice),
NegotiationMin: numericValue(negotiationMin),
NegotiationMax: numericValue(negotiationMax),
ValidationUsed: validationUsed,
DeliveryMode: strings.TrimSpace(input.DeliveryMode),
OrderStatus: "待确认",
})
if err != nil {
return OrderCreateResult{}, internalError("create order failed", err)
}
return OrderCreateResult{
OrderID: order.ID.String(),
OrderStatus: order.OrderStatus,
}, nil
}
func (s *Service) GetOrder(ctx context.Context, orderID string) (Order, error) {
id, err := parseUUID(orderID)
if err != nil {
return Order{}, err
}
order, err := s.queries.GetOrder(ctx, id)
if err != nil {
return Order{}, handleQueryError(err, "order not found")
}
return toOrder(order), nil
}
func buildOrderListParams(input OrderListInput) repository.ListOrdersParams {
params := repository.ListOrdersParams{}
page := normalizePage(input.Limit, input.Offset)
params.Limit, params.Offset = page.Limit, page.Offset
params.OrderStatus = textValue(&input.OrderStatus)
return params
}
func (s *Service) ListOrders(ctx context.Context, input OrderListInput) (ListResult[Order], error) {
params := buildOrderListParams(input)
total, err := s.queries.CountOrders(ctx, params.OrderStatus)
if err != nil {
return ListResult[Order]{}, internalError("count orders failed", err)
}
items, err := s.queries.ListOrders(ctx, params)
if err != nil {
return ListResult[Order]{}, internalError("list orders failed", err)
}
result := ListResult[Order]{
List: make([]Order, 0, len(items)),
Total: total,
Limit: params.Limit,
Offset: params.Offset,
}
for _, item := range items {
result.List = append(result.List, toOrder(item))
}
return result, nil
}
func (s *Service) UpdateOrderStatus(ctx context.Context, orderID string, update StatusUpdate) (OrderCreateResult, error) {
if strings.TrimSpace(update.Status) == "" {
return OrderCreateResult{}, badRequest("order_status is required")
}
if _, ok := allowedOrderStatuses[update.Status]; !ok {
return OrderCreateResult{}, badRequest("order_status is invalid")
}
id, err := parseUUID(orderID)
if err != nil {
return OrderCreateResult{}, err
}
order, err := s.queries.UpdateOrderStatus(ctx, repository.UpdateOrderStatusParams{
ID: id,
OrderStatus: update.Status,
})
if err != nil {
return OrderCreateResult{}, handleQueryError(err, "order not found")
}
return OrderCreateResult{
OrderID: order.ID.String(),
OrderStatus: order.OrderStatus,
}, nil
}
type normalizedPage struct {
Limit int32
Offset int32
}
type derivedAssetFields struct {
QualityLevel string
ScarcityLevel string
BaseValueScore float64
BasePriceMin float64
BasePriceMax float64
}
type derivedPricingFields struct {
ScenarioValueScore float64
ScenarioPriceMin float64
ScenarioPriceMax float64
SuggestedPrice float64
SuccessProbability float64
PricingReason1 string
PricingReason2 string
PricingReason3 string
VerificationSuggestion string
}
type derivedValidationFields struct {
ValidationSignal string
ValidationScore float64
RiskWarning string
ContinueRecommendation string
}
func buildAssetListParams(input AssetListInput) repository.ListDataAssetsParams {
page := normalizePage(input.Limit, input.Offset)
return repository.ListDataAssetsParams{
Limit: page.Limit,
Offset: page.Offset,
Keyword: optionalText(input.Keyword),
AssetType: optionalText(input.AssetType),
Domain: optionalText(input.Domain),
PrivacyLevel: optionalText(input.PrivacyLevel),
SupportsValidation: optionalBool(input.SupportsValidation),
}
}
func normalizePage(limit, offset int32) normalizedPage {
switch {
case limit <= 0:
limit = defaultLimit
case limit > maxLimit:
limit = maxLimit
}
if offset < 0 {
offset = 0
}
return normalizedPage{
Limit: limit,
Offset: offset,
}
}
func validateAssetInput(input AssetCreateInput) error {
switch {
case strings.TrimSpace(input.AssetName) == "":
return badRequest("asset_name is required")
case strings.TrimSpace(input.AssetType) == "":
return badRequest("asset_type is required")
case strings.TrimSpace(input.Domain) == "":
return badRequest("domain is required")
case strings.TrimSpace(input.DataDescription) == "":
return badRequest("data_description is required")
case strings.TrimSpace(input.DataScale) == "":
return badRequest("data_scale is required")
case strings.TrimSpace(input.CollectionMethod) == "":
return badRequest("collection_method is required")
case strings.TrimSpace(input.PrivacyLevel) == "":
return badRequest("privacy_level is required")
case strings.TrimSpace(input.PermissionMode) == "":
return badRequest("permission_mode is required")
case input.SellerExpectedPriceMin == nil:
return badRequest("seller_expected_price_min is required")
case input.SellerExpectedPriceMax == nil:
return badRequest("seller_expected_price_max is required")
case *input.SellerExpectedPriceMin < 0 || *input.SellerExpectedPriceMax < 0:
return badRequest("seller expected prices must be non-negative")
case *input.SellerExpectedPriceMax < *input.SellerExpectedPriceMin:
return badRequest("seller_expected_price_max must be greater than or equal to seller_expected_price_min")
}
return nil
}
func validatePricingInput(input PricingCreateInput) error {
switch {
case strings.TrimSpace(input.AssetID) == "":
return badRequest("asset_id is required")
case strings.TrimSpace(input.TaskType) == "":
return badRequest("task_type is required")
case strings.TrimSpace(input.ModelType) == "":
return badRequest("model_type is required")
case input.BuyerBudgetMin == nil:
return badRequest("buyer_budget_min is required")
case input.BuyerBudgetMax == nil:
return badRequest("buyer_budget_max is required")
case *input.BuyerBudgetMin < 0 || *input.BuyerBudgetMax < 0:
return badRequest("buyer budgets must be non-negative")
case *input.BuyerBudgetMax < *input.BuyerBudgetMin:
return badRequest("buyer_budget_max must be greater than or equal to buyer_budget_min")
}
return nil
}
func validateOrderInput(input OrderCreateInput) error {
switch {
case strings.TrimSpace(input.AssetID) == "":
return badRequest("asset_id is required")
case strings.TrimSpace(input.RequestID) == "":
return badRequest("request_id is required")
case strings.TrimSpace(input.PricingID) == "":
return badRequest("pricing_id is required")
case input.CurrentPrice == nil:
return badRequest("current_price is required")
case *input.CurrentPrice < 0:
return badRequest("current_price must be non-negative")
case strings.TrimSpace(input.DeliveryMode) == "":
return badRequest("delivery_mode is required")
}
return nil
}
func buildAssetDerivedFields(input AssetCreateInput) (derivedAssetFields, error) {
baseMin := *input.SellerExpectedPriceMin
baseMax := *input.SellerExpectedPriceMax
midPrice := (baseMin + baseMax) / 2
qualityScore := 60.0
switch {
case strings.Contains(input.CollectionMethod, "真实"):
qualityScore += 10
case strings.Contains(input.CollectionMethod, "混合"):
qualityScore += 6
default:
qualityScore += 3
}
switch {
case input.LabelingStatus != nil && strings.Contains(*input.LabelingStatus, "完整"):
qualityScore += 8
case input.LabelingStatus != nil && strings.Contains(*input.LabelingStatus, "部分"):
qualityScore += 4
}
if input.SupportsValidation {
qualityScore += 5
}
if strings.Contains(input.PrivacyLevel, "低") {
qualityScore += 4
}
scarcityScore := 12.0
switch strings.TrimSpace(input.Domain) {
case "机器人", "医疗", "制造":
scarcityScore = 18
case "金融", "教育":
scarcityScore = 15
}
if strings.Contains(strings.TrimSpace(input.AssetType), "多模态") || strings.Contains(strings.TrimSpace(input.AssetType), "视频") {
scarcityScore += 4
}
score := clamp(qualityScore+scarcityScore+midPrice/10000, 55, 95)
qualityLevel := "中"
if score >= 82 {
qualityLevel = "高"
} else if score <= 68 {
qualityLevel = "基础"
}
scarcityLevel := "中"
if scarcityScore >= 18 {
scarcityLevel = "高"
} else if scarcityScore <= 12 {
scarcityLevel = "一般"
}
buffer := 0.08
if input.SupportsValidation {
buffer = 0.05
}
return derivedAssetFields{
QualityLevel: qualityLevel,
ScarcityLevel: scarcityLevel,
BaseValueScore: round2(score),
BasePriceMin: round2(baseMin * (1 - buffer)),
BasePriceMax: round2(baseMax * (1 + buffer)),
}, nil
}
// TODO: use real agent
func buildPricingDerivedFields(asset repository.DataAsset, request repository.BuyerRequest) (derivedPricingFields, error) {
baseMin, err := numericToFloat(asset.BasePriceMin)
if err != nil {
return derivedPricingFields{}, badRequest("asset base_price_min is invalid")
}
baseMax, err := numericToFloat(asset.BasePriceMax)
if err != nil {
return derivedPricingFields{}, badRequest("asset base_price_max is invalid")
}
budgetMin, err := numericToFloat(request.BuyerBudgetMin)
if err != nil {
return derivedPricingFields{}, badRequest("buyer_budget_min is invalid")
}
budgetMax, err := numericToFloat(request.BuyerBudgetMax)
if err != nil {
return derivedPricingFields{}, badRequest("buyer_budget_max is invalid")
}
baseScore, err := numericToFloat(asset.BaseValueScore)
if err != nil {
baseScore = 70
}
matchBonus := 6.0
if request.PrivacyRequirement.Valid && request.PrivacyRequirement.String == asset.PrivacyLevel {
matchBonus += 4
}
if request.UsagePurpose.Valid && strings.Contains(request.UsagePurpose.String, "微调") {
matchBonus += 2
}
if strings.Contains(request.TaskType, "抓取") || strings.Contains(request.TaskType, "视觉") {
matchBonus += 3
}
scenarioScore := clamp(baseScore+matchBonus, 60, 97)
priceFloor := math.Max(baseMin, budgetMin*0.95)
priceCeil := math.Max(priceFloor+1000, math.Min(baseMax*1.12, budgetMax*1.08))
suggested := clamp((priceFloor+priceCeil)/2, priceFloor, priceCeil)
successProbability := clamp(0.58+(budgetMax-suggested)/math.Max(suggested, 1)*0.35, 0.35, 0.92)
verificationSuggestion := "建议验证"
if asset.SupportsValidation && suggested <= budgetMax {
verificationSuggestion = "建议先验证后成交"
} else if !asset.SupportsValidation {
verificationSuggestion = "可直接进入询价"
}
return derivedPricingFields{
ScenarioValueScore: round2(scenarioScore),
ScenarioPriceMin: round2(priceFloor),
ScenarioPriceMax: round2(priceCeil),
SuggestedPrice: round2(suggested),
SuccessProbability: round4(successProbability),
PricingReason1: fmt.Sprintf("任务类型“%s”与该资产场景匹配度较高", request.TaskType),
PricingReason2: fmt.Sprintf("资产基础估值区间为 %.0f - %.0f", baseMin, baseMax),
PricingReason3: fmt.Sprintf("买家预算区间为 %.0f - %.0f,当前建议价格可接受", budgetMin, budgetMax),
VerificationSuggestion: verificationSuggestion,
}, nil
}
// TODO: use real agent
func buildValidationDerivedFields(asset repository.DataAsset, request repository.BuyerRequest) (derivedValidationFields, error) {
score := 0.74
signal := "正向"
riskWarning := "风险可控,建议继续推进"
recommendation := "建议继续成交"
if strings.Contains(asset.PrivacyLevel, "高") {
score -= 0.08
riskWarning = "隐私等级较高,建议采用授权访问与受控验证"
}
if request.PrivacyRequirement.Valid && strings.Contains(request.PrivacyRequirement.String, "高") {
score += 0.04
}
if !asset.SupportsValidation {
score -= 0.12
signal = "谨慎"
recommendation = "建议补充验证条件后再推进"
}
if score < 0.68 {
signal = "谨慎"
}
return derivedValidationFields{
ValidationSignal: signal,
ValidationScore: round4(clamp(score, 0.4, 0.95)),
RiskWarning: riskWarning,
ContinueRecommendation: recommendation,
}, nil
}
// TODO: use real agent
func buildNegotiationRange(pricing repository.PricingResult, currentPrice float64) (float64, float64, error) {
priceMin, err := numericToFloat(pricing.ScenarioPriceMin)
if err != nil {
return 0, 0, badRequest("pricing scenario_price_min is invalid")
}
priceMax, err := numericToFloat(pricing.ScenarioPriceMax)
if err != nil {
return 0, 0, badRequest("pricing scenario_price_max is invalid")
}
min := math.Max(priceMin, currentPrice*0.92)
max := math.Min(priceMax, currentPrice*1.08)
if max < min {
max = min
}
return round2(min), round2(max), nil
}
func handleQueryError(err error, notFoundMessage string) error {
switch {
case errors.Is(err, pgx.ErrNoRows):
return notFound(notFoundMessage)
default:
return internalError("database query failed", err)
}
}
func parseUUID(value string) (pgtype.UUID, error) {
var id pgtype.UUID
if err := id.Scan(strings.TrimSpace(value)); err != nil {
return pgtype.UUID{}, badRequest("invalid id")
}
return id, nil
}
func textValue(value *string) pgtype.Text {
if value == nil {
return pgtype.Text{}
}
trimmed := strings.TrimSpace(*value)
if trimmed == "" {
return pgtype.Text{}
}
return pgtype.Text{
String: trimmed,
Valid: true,
}
}
func optionalText(value *string) pgtype.Text {
if value == nil {
return pgtype.Text{}
}
return textValue(value)
}
func optionalBool(value *bool) pgtype.Bool {
if value == nil {
return pgtype.Bool{}
}
return pgtype.Bool{
Bool: *value,
Valid: true,
}
}
func numericValue(value float64) pgtype.Numeric {
var numeric pgtype.Numeric
_ = numeric.ScanScientific(strconv.FormatFloat(round4(value), 'f', -1, 64))
return numeric
}
func numericToFloat(value pgtype.Numeric) (float64, error) {
floatValue, err := value.Float64Value()
if err != nil {
return 0, err
}
if !floatValue.Valid {
return 0, errors.New("numeric value is null")
}
return floatValue.Float64, nil
}
func timestamptzValue(value time.Time) pgtype.Timestamptz {
return pgtype.Timestamptz{
Time: value.UTC(),
Valid: true,
}
}
func toAsset(asset repository.DataAsset) Asset {
return Asset{
ID: asset.ID.String(),
AssetName: asset.AssetName,
AssetType: asset.AssetType,
Domain: asset.Domain,
ApplicationScene: toTextPointer(asset.ApplicationScene),
DataDescription: asset.DataDescription,
DataScale: asset.DataScale,
CollectionMethod: asset.CollectionMethod,
LabelingStatus: toTextPointer(asset.LabelingStatus),
UpdateFrequency: toTextPointer(asset.UpdateFrequency),
PrivacyLevel: asset.PrivacyLevel,
PermissionMode: asset.PermissionMode,
SupportsValidation: asset.SupportsValidation,
SellerExpectedPriceMin: toNumericPointer(asset.SellerExpectedPriceMin),
SellerExpectedPriceMax: toNumericPointer(asset.SellerExpectedPriceMax),
QualityLevel: toTextPointer(asset.QualityLevel),
ScarcityLevel: toTextPointer(asset.ScarcityLevel),
BaseValueScore: toNumericPointer(asset.BaseValueScore),
BasePriceMin: toNumericPointer(asset.BasePriceMin),
BasePriceMax: toNumericPointer(asset.BasePriceMax),
AssetStatus: asset.AssetStatus,
CreatedAt: toTimePointer(asset.CreatedAt),
UpdatedAt: toTimePointer(asset.UpdatedAt),
AgentAssetSummary: toTextPointer(asset.AgentAssetSummary),
AgentRecommendedTasks: toStringArray(asset.AgentRecommendedTasks),
AgentRiskPerMissionAdvice: toTextPointer(asset.AgentRiskPerMissionAdvice),
AgentAssetExplanation: toTextPointer(asset.AgentAssetExplanation),
}
}
func toPricing(pricing repository.PricingResult) Pricing {
return Pricing{
ID: pricing.ID.String(),
AssetID: pricing.AssetID.String(),
RequestID: pricing.RequestID.String(),
ScenarioValueScore: toNumericPointer(pricing.ScenarioValueScore),
ScenarioPriceMin: toNumericPointer(pricing.ScenarioPriceMin),
ScenarioPriceMax: toNumericPointer(pricing.ScenarioPriceMax),
SuggestedPrice: toNumericPointer(pricing.SuggestedPrice),
SuccessProbability: toNumericPointer(pricing.SuccessProbability),
PricingReason1: toTextPointer(pricing.PricingReason1),
PricingReason2: toTextPointer(pricing.PricingReason2),
PricingReason3: toTextPointer(pricing.PricingReason3),
VerificationSuggestion: toTextPointer(pricing.VerificationSuggestion),
PricingStatus: pricing.PricingStatus,
CreatedAt: toTimePointer(pricing.CreatedAt),
UpdatedAt: toTimePointer(pricing.UpdatedAt),
}
}
func toValidation(validation repository.Validation) Validation {
return Validation{
ID: validation.ID.String(),
AssetID: validation.AssetID.String(),
RequestID: validation.RequestID.String(),
ValidationType: toTextPointer(validation.ValidationType),
ValidationRequested: validation.ValidationRequested,
ValidationStatus: validation.ValidationStatus,
ValidationSignal: toTextPointer(validation.ValidationSignal),
ValidationScore: toNumericPointer(validation.ValidationScore),
RiskWarning: toTextPointer(validation.RiskWarning),
ContinueRecommendation: toTextPointer(validation.ContinueRecommendation),
ValidationCreatedAt: toTimePointer(validation.ValidationCreatedAt),
ValidationFinishedAt: toTimePointer(validation.ValidationFinishedAt),
}
}
func toOrder(order repository.Order) Order {
validationID := ""
if order.ValidationID.Valid {
validationID = order.ValidationID.String()
}
return Order{
ID: order.ID.String(),
AssetID: order.AssetID.String(),
RequestID: order.RequestID.String(),
PricingID: order.PricingID.String(),
ValidationID: validationID,
AssetName: order.AssetName,
CurrentPrice: toNumericPointer(order.CurrentPrice),
NegotiationMin: toNumericPointer(order.NegotiationMin),
NegotiationMax: toNumericPointer(order.NegotiationMax),
ValidationUsed: order.ValidationUsed,
DeliveryMode: order.DeliveryMode,
OrderStatus: order.OrderStatus,
OrderCreatedAt: toTimePointer(order.OrderCreatedAt),
OrderUpdatedAt: toTimePointer(order.OrderUpdatedAt),
}
}
func toTextPointer(value pgtype.Text) *string {
if !value.Valid {
return nil
}
return new(value.String)
}
func toStringArray(value []byte) []string {
var result []string
if err := json.Unmarshal(value, &result); err != nil {
slog.Warn(err.Error(), "value", string(value))
}
return result
}
func toNumericPointer(value pgtype.Numeric) *float64 {
if !value.Valid {
return nil
}
floatValue, err := value.Float64Value()
if err != nil || !floatValue.Valid {
return nil
}
return new(round4(floatValue.Float64))
}
func toTimePointer(value pgtype.Timestamptz) *time.Time {
if !value.Valid {
return nil
}
return new(value.Time)
}
func clamp(value, min, max float64) float64 {
if value < min {
return min
}
if value > max {
return max
}
return value
}
func round2(value float64) float64 {
return math.Round(value*100) / 100
}
func round4(value float64) float64 {
return math.Round(value*10000) / 10000
}