Files
sub2api/backend/internal/service/batch_image_settlement_test.go
T

514 lines
22 KiB
Go
Raw Normal View History

//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