Files
sub2api/backend/internal/service/batch_image_settlement_test.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

514 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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