90 lines
3.0 KiB
Go
90 lines
3.0 KiB
Go
package billing
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
ErrInsufficientBalance = errors.New("insufficient billing balance")
|
|
ErrIdempotencyConflict = errors.New("billing idempotency conflict")
|
|
)
|
|
|
|
type WalletPostParams struct {
|
|
LedgerID, OrganizationID, AccountID, JobID, Kind string
|
|
DeltaFen int64
|
|
Currency, IdempotencyKey, Description string
|
|
Metadata map[string]any
|
|
}
|
|
type WalletPosting struct {
|
|
LedgerID string
|
|
BalanceAfterFen, BalanceFen, TotalRechargedFen, TotalChargedFen, DeltaFen int64
|
|
CreatedAt, UpdatedAt time.Time
|
|
}
|
|
type WalletPoster interface {
|
|
PostWalletEntry(context.Context, WalletPostParams) (WalletPosting, error)
|
|
}
|
|
type Ledger struct {
|
|
Poster WalletPoster
|
|
NewID func() string
|
|
}
|
|
type ChargeRequest struct {
|
|
OrganizationID, AccountID, JobID, Description string
|
|
AmountFen int64
|
|
Metadata map[string]any
|
|
}
|
|
type RefundRequest = ChargeRequest
|
|
|
|
type StatusError struct {
|
|
Status int
|
|
Err error
|
|
}
|
|
|
|
func (e *StatusError) Error() string { return e.Err.Error() }
|
|
func (e *StatusError) Unwrap() error { return e.Err }
|
|
func HTTPStatus(err error) int {
|
|
var status *StatusError
|
|
if errors.As(err, &status) {
|
|
return status.Status
|
|
}
|
|
return 500
|
|
}
|
|
|
|
func (l Ledger) Charge(ctx context.Context, in ChargeRequest) (WalletPosting, error) {
|
|
return l.post(ctx, in, "charge", -in.AmountFen, "job-charge:"+in.JobID)
|
|
}
|
|
func (l Ledger) Refund(ctx context.Context, in RefundRequest) (WalletPosting, error) {
|
|
return l.post(ctx, in, "refund", in.AmountFen, "job-refund:"+in.JobID)
|
|
}
|
|
func (l Ledger) post(ctx context.Context, in ChargeRequest, kind string, delta int64, key string) (WalletPosting, error) {
|
|
if l.Poster == nil || l.NewID == nil {
|
|
return WalletPosting{}, errors.New("billing ledger is not configured")
|
|
}
|
|
if in.AmountFen <= 0 {
|
|
return WalletPosting{}, errors.New("billing amount must be positive")
|
|
}
|
|
posting, err := l.Poster.PostWalletEntry(ctx, WalletPostParams{LedgerID: l.NewID(), OrganizationID: in.OrganizationID, AccountID: in.AccountID, JobID: in.JobID, Kind: kind, DeltaFen: delta, Currency: CurrencyCNY, IdempotencyKey: key, Description: in.Description, Metadata: in.Metadata})
|
|
if err == nil {
|
|
return posting, nil
|
|
}
|
|
message := err.Error()
|
|
if strings.Contains(message, "BILLING_INSUFFICIENT_BALANCE") {
|
|
return WalletPosting{}, &StatusError{402, fmt.Errorf("%w: %v", ErrInsufficientBalance, err)}
|
|
}
|
|
if strings.Contains(message, "BILLING_IDEMPOTENCY_PAYLOAD_MISMATCH") {
|
|
return WalletPosting{}, &StatusError{409, fmt.Errorf("%w: %v", ErrIdempotencyConflict, err)}
|
|
}
|
|
return WalletPosting{}, err
|
|
}
|
|
|
|
func encodeMetadata(value map[string]any) (json.RawMessage, error) {
|
|
if value == nil {
|
|
return json.RawMessage(nil), nil
|
|
}
|
|
return json.Marshal(value)
|
|
}
|