157 lines
5.5 KiB
Go
157 lines
5.5 KiB
Go
package billing
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Wallet struct {
|
|
OrganizationID string `json:"organizationId"`
|
|
BalanceFen int64 `json:"balanceFen"`
|
|
TotalRechargedFen int64 `json:"totalRechargedFen"`
|
|
TotalChargedFen int64 `json:"totalChargedFen"`
|
|
Currency string `json:"currency,omitempty"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
}
|
|
|
|
type LedgerEntry struct {
|
|
ID string `json:"id"`
|
|
OrganizationID string `json:"organizationId"`
|
|
AccountID string `json:"accountId,omitempty"`
|
|
JobID string `json:"jobId,omitempty"`
|
|
Kind string `json:"kind"`
|
|
DeltaFen int64 `json:"deltaFen"`
|
|
BalanceAfterFen int64 `json:"balanceAfterFen"`
|
|
Currency string `json:"currency"`
|
|
IdempotencyKey string `json:"idempotencyKey"`
|
|
Description string `json:"description"`
|
|
Metadata map[string]any `json:"metadata"`
|
|
CreatedAt string `json:"createdAt"`
|
|
}
|
|
|
|
type Summary struct {
|
|
RechargeFen int64 `json:"rechargeFen"`
|
|
ChargedFen int64 `json:"chargedFen"`
|
|
RefundedFen int64 `json:"refundedFen"`
|
|
NetConsumedFen int64 `json:"netConsumedFen"`
|
|
}
|
|
type AccountConfig struct {
|
|
AccountName string `json:"accountName,omitempty"`
|
|
BankName string `json:"bankName,omitempty"`
|
|
AccountNumber string `json:"accountNumber,omitempty"`
|
|
Contact string `json:"contact,omitempty"`
|
|
}
|
|
type Organization struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
ArchiveOwnerID string `json:"archiveOwnerId"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
Wallet Wallet `json:"wallet"`
|
|
}
|
|
type Member struct {
|
|
ID string `json:"id"`
|
|
DisplayName string `json:"displayName"`
|
|
Phone string `json:"phone"`
|
|
Role string `json:"role"`
|
|
OrganizationID string `json:"organizationId,omitempty"`
|
|
Status string `json:"status"`
|
|
}
|
|
type Overview struct {
|
|
Wallet Wallet `json:"wallet"`
|
|
Ledger []LedgerEntry `json:"ledger"`
|
|
BillingAccount AccountConfig `json:"billingAccount"`
|
|
Summary Summary `json:"summary"`
|
|
Personal Summary `json:"personal"`
|
|
}
|
|
type AdminOverview struct {
|
|
BillingAccount AccountConfig `json:"billingAccount"`
|
|
Organizations []Organization `json:"organizations"`
|
|
Members []Member `json:"members"`
|
|
Ledger []LedgerEntry `json:"ledger"`
|
|
PriceRules []PriceRule `json:"priceRules"`
|
|
}
|
|
|
|
type QuoteCommand struct {
|
|
AccountID, OrganizationID, OrganizationName, Role string
|
|
Provider, Capability, ReqKey string
|
|
Parameters Parameters
|
|
Payload map[string]any
|
|
}
|
|
type PricePatch struct {
|
|
MarkupMultiplier float64
|
|
DimensionKey, TierValue string
|
|
}
|
|
type AdjustmentCommand struct {
|
|
OrganizationID, OperatorID, Direction, Note string
|
|
AmountFen, DeltaFen int64
|
|
}
|
|
type AdjustmentResult struct {
|
|
Wallet Wallet
|
|
Entry LedgerEntry
|
|
}
|
|
|
|
type AccountConfigStore interface {
|
|
Load(context.Context) (AccountConfig, error)
|
|
Save(context.Context, AccountConfig) error
|
|
}
|
|
|
|
// ReadService is the deliberately cohesive persistence seam required by the
|
|
// Billing HTTP module. Implementations can execute overview reads concurrently.
|
|
type ReadService interface {
|
|
Overview(context.Context, string, string) (Overview, error)
|
|
AdminOverview(context.Context) (AdminOverview, error)
|
|
ListPrices(context.Context) ([]PriceRule, error)
|
|
GetPrice(context.Context, string) (*PriceRule, error)
|
|
}
|
|
type HTTPService interface {
|
|
ReadService
|
|
Quote(context.Context, QuoteCommand) (*Quote, error)
|
|
UpdatePrice(context.Context, string, PricePatch) (*PriceRule, error)
|
|
Adjust(context.Context, AdjustmentCommand) (AdjustmentResult, error)
|
|
}
|
|
|
|
var ErrPriceNotFound = errors.New("billing price not found")
|
|
|
|
func ValidatePricePatch(rule *PriceRule, patch PricePatch) error {
|
|
if patch.MarkupMultiplier < 1 || patch.MarkupMultiplier > 1000 || math.IsNaN(patch.MarkupMultiplier) || math.IsInf(patch.MarkupMultiplier, 0) {
|
|
return errors.New("上浮倍率必须在 1.00 至 1000.00 之间。")
|
|
}
|
|
if (patch.DimensionKey == "") != (patch.TierValue == "") {
|
|
return errors.New("参数档位倍率更新必须同时提供参数维度和档位。")
|
|
}
|
|
if rule == nil {
|
|
return ErrPriceNotFound
|
|
}
|
|
if patch.DimensionKey == "" && len(rule.Dimensions) > 0 {
|
|
return errors.New("当前服务包含参数档位,请指定要调整的参数档位倍率。")
|
|
}
|
|
if patch.DimensionKey != "" {
|
|
for _, dimension := range rule.Dimensions {
|
|
if dimension.Key != patch.DimensionKey {
|
|
continue
|
|
}
|
|
for _, tier := range dimension.Tiers {
|
|
if strings.TrimSpace(fmt.Sprint(tier.Value)) == patch.TierValue {
|
|
return nil
|
|
}
|
|
}
|
|
return errors.New("计费参数档位不存在。")
|
|
}
|
|
return errors.New("计费参数维度不存在。")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func PostingResult(post WalletPosting, organizationID string) AdjustmentResult {
|
|
return AdjustmentResult{
|
|
Wallet: Wallet{OrganizationID: organizationID, BalanceFen: post.BalanceFen, TotalRechargedFen: post.TotalRechargedFen, TotalChargedFen: post.TotalChargedFen, Currency: CurrencyCNY, UpdatedAt: post.UpdatedAt.Format(time.RFC3339Nano)},
|
|
Entry: LedgerEntry{ID: post.LedgerID, OrganizationID: organizationID, DeltaFen: post.DeltaFen, BalanceAfterFen: post.BalanceAfterFen, Currency: CurrencyCNY, CreatedAt: post.CreatedAt.Format(time.RFC3339Nano)},
|
|
}
|
|
}
|