//go:build unit package service import ( "context" "errors" "fmt" "strings" "testing" "github.com/stretchr/testify/require" ) func TestBatchImageSettlementService_SettlesAndChargesSuccessfulImagesOnly(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_settle") job.SuccessCount = 3 job.FailCount = 2 job.ItemCount = 5 job.SessionID = batchImageStringPtr("batch-settlement-session") repo.jobs[job.BatchID] = job billing := &fakeBatchImageBillingRepo{} usageLogs := &openAIRecordUsageLogRepoStub{} svc := &BatchImageSettlementService{ Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.25}, UsageLogRepo: usageLogs, } result, err := svc.Settle(context.Background(), job.BatchID) require.NoError(t, err) require.Equal(t, 0.75, result.ActualCost) require.Equal(t, BatchImageCaptureRequestID(job.BatchID), result.RequestID) require.False(t, result.AlreadySettled) require.Equal(t, BatchImageJobStatusCompleted, repo.jobs[job.BatchID].Status) require.NotNil(t, repo.jobs[job.BatchID].ActualCost) require.Equal(t, 0.75, *repo.jobs[job.BatchID].ActualCost) require.NotEmpty(t, batchImageDerefString(repo.jobs[job.BatchID].ManifestHash)) require.NotNil(t, repo.jobs[job.BatchID].SettledAt) require.Equal(t, "batch-settlement-session", batchImageDerefString(usageLogs.lastLog.SessionID)) require.Len(t, billing.captures, 1) require.Equal(t, int64(321), billing.captures[0].APIKeyID) require.Equal(t, job.UserID, billing.captures[0].UserID) require.Equal(t, job.BatchID, billing.captures[0].BatchID) require.Equal(t, 0.75, billing.captures[0].ActualAmount) require.Equal(t, 1.25, billing.captures[0].HoldAmount) require.NotContains(t, fmt.Sprintf("%+v", billing.captures[0]), batchImageTestData) require.NotContains(t, fmt.Sprintf("%+v", billing.captures[0]), "gs://") require.NotContains(t, fmt.Sprintf("%+v", billing.captures[0]), "prompt") } func TestBatchImageSettlementService_ZeroSuccessCanComplete(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_zero") job.SuccessCount = 0 job.FailCount = 4 job.ItemCount = 4 repo.jobs[job.BatchID] = job billing := &fakeBatchImageBillingRepo{} svc := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.25}} result, err := svc.Settle(context.Background(), job.BatchID) require.NoError(t, err) require.Equal(t, 0.0, result.ActualCost) require.Equal(t, BatchImageJobStatusCompleted, repo.jobs[job.BatchID].Status) require.Len(t, billing.captures, 1) require.Equal(t, 0.0, billing.captures[0].ActualAmount) } func TestBatchImageSettlementService_CompletedJobReturnsAlreadySettledWithoutBilling(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_done") job.Status = BatchImageJobStatusCompleted cost := 0.5 job.ActualCost = &cost repo.jobs[job.BatchID] = job billing := &fakeBatchImageBillingRepo{} svc := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.25}} result, err := svc.Settle(context.Background(), job.BatchID) require.NoError(t, err) require.True(t, result.AlreadySettled) require.Equal(t, 0.5, result.ActualCost) require.Empty(t, billing.captures) } func TestBatchImageSettlementService_IdempotentAfterBillingCrash(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_crash") repo.jobs[job.BatchID] = job billing := &fakeBatchImageBillingRepo{alreadyApplied: map[string]bool{BatchImageCaptureRequestID(job.BatchID): true}} svc := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.25}} result, err := svc.Settle(context.Background(), job.BatchID) require.NoError(t, err) require.Equal(t, 0.5, result.ActualCost) require.Equal(t, BatchImageJobStatusCompleted, repo.jobs[job.BatchID].Status) require.Len(t, billing.captures, 1) } func TestBatchImageSettlementService_ValidationErrors(t *testing.T) { tests := []struct { name string mutate func(*BatchImageJob) pricing BatchImagePricingResolver want error }{ {name: "invalid_status", mutate: func(j *BatchImageJob) { j.Status = BatchImageJobStatusRunning }, want: ErrBatchImageSettlementInvalidStatus}, {name: "negative_success_count", mutate: func(j *BatchImageJob) { j.SuccessCount = -1 }, want: ErrBatchImageSettlementInvalidCounts}, {name: "negative_fail_count", mutate: func(j *BatchImageJob) { j.FailCount = -1 }, want: ErrBatchImageSettlementInvalidCounts}, {name: "counts_exceed_item_count", mutate: func(j *BatchImageJob) { j.SuccessCount = 2; j.FailCount = 2; j.ItemCount = 3 }, want: ErrBatchImageSettlementInvalidCounts}, {name: "missing_api_key", mutate: func(j *BatchImageJob) { j.APIKeyID = nil }, want: ErrBatchImageSettlementMissingAPIKeyID}, {name: "missing_account", mutate: func(j *BatchImageJob) { j.AccountID = nil }, want: ErrBatchImageSettlementMissingAccountID}, {name: "pricing_missing", pricing: &fakeBatchImagePricingResolver{err: ErrBatchImageSettlementPricingMissing}, want: ErrBatchImageSettlementPricingMissing}, {name: "manifest_conflict", mutate: func(j *BatchImageJob) { v := "different"; j.ManifestHash = &v }, want: ErrBatchImageSettlementManifestConflict}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_" + tt.name) if tt.mutate != nil { tt.mutate(job) } repo.jobs[job.BatchID] = job pricing := tt.pricing if pricing == nil { pricing = &fakeBatchImagePricingResolver{unitPrice: 0.25} } billing := &fakeBatchImageBillingRepo{} svc := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: pricing} _, err := svc.Settle(context.Background(), job.BatchID) require.ErrorIs(t, err, tt.want) require.Empty(t, billing.captures) require.NotEqual(t, BatchImageJobStatusCompleted, repo.jobs[job.BatchID].Status) }) } } func TestBatchImageSettlementService_CostExceedingHoldDoesNotCharge(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_cost_over_hold") job.SuccessCount = 2 job.FailCount = 0 job.ItemCount = 2 holdAmount := 0.5 job.HoldAmount = &holdAmount job.EstimatedCost = holdAmount repo.jobs[job.BatchID] = job billing := &fakeBatchImageBillingRepo{} svc := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.50}} _, err := svc.Settle(context.Background(), job.BatchID) require.ErrorIs(t, err, ErrBatchImageSettlementCostExceedsHold) require.Empty(t, billing.captures) require.Equal(t, BatchImageJobStatusSettling, repo.jobs[job.BatchID].Status) require.Equal(t, "SETTLEMENT_COST_EXCEEDS_HOLD", batchImageDerefString(repo.jobs[job.BatchID].LastErrorCode)) } func TestBatchImageSettlementService_UsesSubmittedPricingSnapshot(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_snapshot") job.SuccessCount = 2 job.FailCount = 0 job.ItemCount = 2 job.PricingSnapshotVersion = 1 job.BaseUnitPrice = 0.25 job.GroupRateMultiplier = 1 job.AccountRateMultiplier = 1 job.BatchDiscountMultiplier = 1 job.HoldMultiplier = 1.1 job.BillableUnitPrice = 0.25 job.HoldUnitPrice = 0.275 holdAmount := 0.55 job.HoldAmount = &holdAmount job.EstimatedCost = 0.5 repo.jobs[job.BatchID] = job billing := &fakeBatchImageBillingRepo{} svc := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.50}} result, err := svc.Settle(context.Background(), job.BatchID) require.NoError(t, err) require.InDelta(t, 0.5, result.ActualCost, 1e-12) require.Len(t, billing.captures, 1) require.InDelta(t, 0.5, billing.captures[0].ActualAmount, 1e-12) require.InDelta(t, 0.55, billing.captures[0].HoldAmount, 1e-12) } func TestBatchImageSettlementService_BillingFailureLeavesSettlingAndRecordsError(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_billing_fail") repo.jobs[job.BatchID] = job billing := &fakeBatchImageBillingRepo{err: errors.New("temporary billing timeout with gs://hidden-output")} svc := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.25}} _, err := svc.Settle(context.Background(), job.BatchID) require.ErrorIs(t, err, ErrBatchImageSettlementBillingFailed) require.Equal(t, BatchImageJobStatusSettling, repo.jobs[job.BatchID].Status) require.Equal(t, "SETTLEMENT_BILLING_FAILED", batchImageDerefString(repo.jobs[job.BatchID].LastErrorCode)) require.Contains(t, batchImageDerefString(repo.jobs[job.BatchID].LastErrorMessage), "temporary billing timeout") require.NotNil(t, billing.captures[0]) } func TestBatchImagePipelineProcessor_SettlesQueuedSettlingJob(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_pipeline") repo.jobs[job.BatchID] = job billing := &fakeBatchImageBillingRepo{} settlement := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.25}} processor := &BatchImagePipelineProcessor{ ProviderProcessor: &BatchImageProviderProcessor{Repo: repo, ProviderRegistry: NewBatchImageProviderRegistry(&fakeProcessorProvider{}), AccountResolver: &fakeBatchImageAccountResolver{account: &Account{}}}, SettlementService: settlement, } result, err := processor.Process(context.Background(), job.BatchID) require.NoError(t, err) require.True(t, result.Terminal) require.Equal(t, BatchImageJobStatusCompleted, repo.jobs[job.BatchID].Status) require.Len(t, billing.captures, 1) } func TestBatchImagePipelineProcessor_RequeuesTransientSettlementFailure(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_pipeline_retry") repo.jobs[job.BatchID] = job settlement := &BatchImageSettlementService{Repo: repo, BillingRepo: &fakeBatchImageBillingRepo{err: errors.New("temporary")}, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.25}} processor := &BatchImagePipelineProcessor{ ProviderProcessor: &BatchImageProviderProcessor{Repo: repo, ProviderRegistry: NewBatchImageProviderRegistry(&fakeProcessorProvider{}), AccountResolver: &fakeBatchImageAccountResolver{account: &Account{}}}, SettlementService: settlement, } result, err := processor.Process(context.Background(), job.BatchID) require.NoError(t, err) require.False(t, result.Terminal) require.Equal(t, batchImageSettlementRetryDelay, result.RequeueAfter) require.Equal(t, BatchImageJobStatusSettling, repo.jobs[job.BatchID].Status) } func TestBatchImagePipelineProcessor_FailsAndReleasesAfterSettlementRetryLimit(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_pipeline_retry_exhausted") job.RetryCount = batchImageSettlementMaxRetries - 1 repo.jobs[job.BatchID] = job billing := &fakeBatchImageBillingRepo{captureErr: errors.New("temporary billing timeout")} settlement := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.25}} processor := &BatchImagePipelineProcessor{ ProviderProcessor: &BatchImageProviderProcessor{Repo: repo, ProviderRegistry: NewBatchImageProviderRegistry(&fakeProcessorProvider{}), AccountResolver: &fakeBatchImageAccountResolver{account: &Account{}}}, SettlementService: settlement, } result, err := processor.Process(context.Background(), job.BatchID) require.NoError(t, err) require.True(t, result.Terminal) require.Equal(t, BatchImageJobStatusFailed, repo.jobs[job.BatchID].Status) require.Equal(t, "SETTLEMENT_BILLING_RETRY_EXHAUSTED", batchImageDerefString(repo.jobs[job.BatchID].LastErrorCode)) require.Len(t, billing.captures, 1) require.Len(t, billing.releases, 1) require.Equal(t, BatchImageReleaseRequestID(job.BatchID), billing.releases[0].RequestID) } func TestBatchImageSettlementRetryExhaustedReleaseIsIdempotentAfterTransitionFailure(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_retry_exhausted_transition_fail") job.RetryCount = batchImageSettlementMaxRetries job.LastErrorCode = batchImageStringPtr("SETTLEMENT_BILLING_FAILED") repo.jobs[job.BatchID] = job repo.transitionErr = errors.New("temporary transition failure") billing := &fakeBatchImageBillingRepo{} svc := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.25}} _, err := svc.Settle(context.Background(), job.BatchID) require.ErrorContains(t, err, "temporary transition failure") require.Equal(t, BatchImageJobStatusSettling, repo.jobs[job.BatchID].Status) require.Len(t, billing.releases, 1) require.Len(t, billing.seen, 1) repo.transitionErr = nil _, err = svc.Settle(context.Background(), job.BatchID) require.ErrorIs(t, err, ErrBatchImageSettlementBillingFailed) require.Equal(t, BatchImageJobStatusFailed, repo.jobs[job.BatchID].Status) require.Len(t, billing.releases, 2) require.Equal(t, billing.releases[0].RequestID, billing.releases[1].RequestID) require.Len(t, billing.seen, 1) } func TestBatchImageSettlementService_CostExceedsHoldExhaustsAndReleases(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_over_hold_exhausted") job.SuccessCount = 2 job.FailCount = 0 job.ItemCount = 2 holdAmount := 0.5 job.HoldAmount = &holdAmount job.EstimatedCost = holdAmount requestHash := "request-hash-over-hold" job.RequestHash = &requestHash repo.jobs[job.BatchID] = job billing := &fakeBatchImageBillingRepo{} svc := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.50}} // 前 N-1 次:记录失败并返回错误(等待 worker 重试)。 for i := 0; i < batchImageSettlementMaxRetries-1; i++ { _, err := svc.Settle(context.Background(), job.BatchID) require.ErrorIs(t, err, ErrBatchImageSettlementCostExceedsHold) require.Equal(t, BatchImageJobStatusSettling, repo.jobs[job.BatchID].Status) } // 达到上限:必须走耗尽出口释放冻结并转 failed,而不是无限 requeue。 _, err := svc.Settle(context.Background(), job.BatchID) require.ErrorIs(t, err, ErrBatchImageSettlementBillingFailed) require.Equal(t, BatchImageJobStatusFailed, repo.jobs[job.BatchID].Status) require.Empty(t, billing.captures) require.Len(t, billing.releases, 1) require.Equal(t, BatchImageReleaseRequestID(job.BatchID), billing.releases[0].RequestID) // 释放指纹必须与 processor/Cancel/recovery 一致地使用 RequestHash, // 否则共享同一 request id 的后续释放会命中指纹冲突(毒消息)。 require.Equal(t, requestHash, billing.releases[0].RequestPayloadHash) } func TestBatchImageSettlementService_InvalidCountsExhaustsAndReleases(t *testing.T) { repo := newFakeBatchImageRepository() job := testSettlingBatchImageJob("imgbatch_bad_counts_exhausted") job.SuccessCount = 2 job.FailCount = 2 job.ItemCount = 3 requestHash := "request-hash-bad-counts" job.RequestHash = &requestHash repo.jobs[job.BatchID] = job billing := &fakeBatchImageBillingRepo{} svc := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.25}} for i := 0; i < batchImageSettlementMaxRetries-1; i++ { _, err := svc.Settle(context.Background(), job.BatchID) require.ErrorIs(t, err, ErrBatchImageSettlementInvalidCounts) } _, err := svc.Settle(context.Background(), job.BatchID) require.ErrorIs(t, err, ErrBatchImageSettlementBillingFailed) require.Equal(t, BatchImageJobStatusFailed, repo.jobs[job.BatchID].Status) require.Empty(t, billing.captures) require.Len(t, billing.releases, 1) require.Equal(t, requestHash, billing.releases[0].RequestPayloadHash) } func TestReleaseBatchImageBalanceHold_TreatsFingerprintConflictAsReleased(t *testing.T) { job := testSettlingBatchImageJob("imgbatch_release_conflict") // 历史版本用 manifestHash 释放过一次:同一 request id 再以 RequestHash // 释放会命中指纹冲突。资金已归还,必须视为幂等成功而非毒消息。 billing := &fakeBatchImageBillingRepo{releaseErr: ErrUsageBillingRequestConflict} err := releaseBatchImageBalanceHold(context.Background(), billing, job, "request-hash") require.NoError(t, err) require.Len(t, billing.releases, 1) } func TestBatchImageSettlementManifestHash(t *testing.T) { job := testSettlingBatchImageJob("imgbatch_hash") first := BuildBatchImageSettlementManifestHash(job) job.CreatedAt = job.CreatedAt.AddDate(0, 0, 1) job.UpdatedAt = job.UpdatedAt.AddDate(0, 0, 1) require.Equal(t, first, BuildBatchImageSettlementManifestHash(job)) job.SuccessCount++ require.NotEqual(t, first, BuildBatchImageSettlementManifestHash(job)) job.SuccessCount-- promptOrBase64 := first + " prompt " + batchImageTestData require.NotContains(t, BuildBatchImageSettlementManifestHash(job), promptOrBase64) } func TestBatchImageSettlementBillingRequestIDs(t *testing.T) { repo := newFakeBatchImageRepository() first := testSettlingBatchImageJob("imgbatch_unique_1") second := testSettlingBatchImageJob("imgbatch_unique_2") repo.jobs[first.BatchID] = first repo.jobs[second.BatchID] = second billing := &fakeBatchImageBillingRepo{} svc := &BatchImageSettlementService{Repo: repo, BillingRepo: billing, Pricing: &fakeBatchImagePricingResolver{unitPrice: 0.25}} _, err := svc.Settle(context.Background(), first.BatchID) require.NoError(t, err) _, err = svc.Settle(context.Background(), first.BatchID) require.NoError(t, err) _, err = svc.Settle(context.Background(), second.BatchID) require.NoError(t, err) require.Len(t, billing.captures, 2) require.Equal(t, BatchImageCaptureRequestID(first.BatchID), billing.captures[0].RequestID) require.Equal(t, BatchImageCaptureRequestID(second.BatchID), billing.captures[1].RequestID) require.NotEqual(t, billing.captures[0].RequestID, billing.captures[1].RequestID) require.Len(t, billing.seen, 2) } func testSettlingBatchImageJob(batchID string) *BatchImageJob { apiKeyID := int64(321) accountID := int64(654) providerJobName := "providers/job" outputRef := "files/output" holdAmount := 1.25 holdID := BatchImageHoldRequestID(batchID) return &BatchImageJob{ BatchID: batchID, UserID: 123, APIKeyID: &apiKeyID, AccountID: &accountID, Provider: BatchImageProviderGeminiAPI, Model: "gemini-image", Status: BatchImageJobStatusSettling, ProviderJobName: &providerJobName, ProviderOutputRef: &outputRef, ItemCount: 3, SuccessCount: 2, FailCount: 1, EstimatedCost: holdAmount, HoldAmount: &holdAmount, HoldID: &holdID, } } type fakeBatchImagePricingResolver struct { unitPrice float64 missingModels map[string]bool err error } func (r *fakeBatchImagePricingResolver) BatchImageUnitPrice(_ context.Context, job *BatchImageJob) (float64, error) { if r.err != nil { return 0, r.err } if job != nil && r.missingModels[job.Model] { return 0, ErrBatchImageSettlementPricingMissing } return r.unitPrice, nil } type fakeBatchImageBillingRepo struct { commands []*UsageBillingCommand reserves []*BatchImageBalanceHoldCommand captures []*BatchImageBalanceHoldCommand releases []*BatchImageBalanceHoldCommand seen map[string]struct{} alreadyApplied map[string]bool err error reserveErr error captureErr error releaseErr error } func (r *fakeBatchImageBillingRepo) Apply(_ context.Context, cmd *UsageBillingCommand) (*UsageBillingApplyResult, error) { if r.seen == nil { r.seen = make(map[string]struct{}) } if r.err != nil { r.commands = append(r.commands, cmd) return nil, r.err } if cmd != nil { cmd.Normalize() if _, ok := r.seen[cmd.RequestID]; ok || r.alreadyApplied[cmd.RequestID] { r.commands = append(r.commands, cmd) return &UsageBillingApplyResult{Applied: false}, nil } r.seen[cmd.RequestID] = struct{}{} } r.commands = append(r.commands, cmd) return &UsageBillingApplyResult{Applied: true}, nil } func (r *fakeBatchImageBillingRepo) ReserveBatchImageBalance(_ context.Context, cmd *BatchImageBalanceHoldCommand) (*BatchImageBalanceHoldResult, error) { if r.reserveErr != nil { r.reserves = append(r.reserves, cmd) return nil, r.reserveErr } return r.applyHold(cmd, &r.reserves) } func (r *fakeBatchImageBillingRepo) CaptureBatchImageBalance(_ context.Context, cmd *BatchImageBalanceHoldCommand) (*BatchImageBalanceHoldResult, error) { if r.captureErr != nil { r.captures = append(r.captures, cmd) return nil, r.captureErr } return r.applyHold(cmd, &r.captures) } func (r *fakeBatchImageBillingRepo) ReleaseBatchImageBalance(_ context.Context, cmd *BatchImageBalanceHoldCommand) (*BatchImageBalanceHoldResult, error) { if r.releaseErr != nil { r.releases = append(r.releases, cmd) return nil, r.releaseErr } return r.applyHold(cmd, &r.releases) } func (r *fakeBatchImageBillingRepo) applyHold(cmd *BatchImageBalanceHoldCommand, calls *[]*BatchImageBalanceHoldCommand) (*BatchImageBalanceHoldResult, error) { if r.seen == nil { r.seen = make(map[string]struct{}) } if r.err != nil { *calls = append(*calls, cmd) return nil, r.err } if cmd != nil { cmd.Normalize() if _, ok := r.seen[cmd.RequestID]; ok || r.alreadyApplied[cmd.RequestID] { *calls = append(*calls, cmd) return &BatchImageBalanceHoldResult{Applied: false}, nil } r.seen[cmd.RequestID] = struct{}{} } *calls = append(*calls, cmd) return &BatchImageBalanceHoldResult{Applied: true}, nil } var _ UsageBillingRepository = (*fakeBatchImageBillingRepo)(nil) var _ BatchImagePricingResolver = (*fakeBatchImagePricingResolver)(nil) var _ = strings.TrimSpace