@@ -2,28 +2,43 @@ package tokenstoreapi
import (
"context"
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"time"
gatewaytokenstore "github.com/LoveLosita/smartflow/backend/client/tokenstore"
"github.com/LoveLosita/smartflow/backend/gateway/shared/respond"
token contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/token store"
credit contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/credit store"
"github.com/gin-gonic/gin"
)
const requestTimeout = 2 * time . Second
const requestTimeout = 5 * time . Second
const (
creditConsumptionPeriod24h = "24h"
creditConsumptionPeriod7d = "7d"
creditConsumptionPeriod30d = "30d"
creditConsumptionPeriodAll = "all"
creditDashboardPageSize = 50
)
// TokenStoreClient 是商店页 credit-store 语义所需的最小依赖面。
//
// 职责边界:
// 1. 只暴露 Credit 商店和流水所需能力,不再承接旧 token 商店接口。
// 2. 所有方法都以“当前登录用户”口径访问,不开放跨用户查询。
// 3. 具体 DB、RPC、Redis 细节统一封装在 tokenstore client 内。
type TokenStoreClient interface {
GetSummary ( ctx context . Context , actorU serID uint64 ) ( * token contracts. TokenSummary , error )
ListProducts ( ctx context . Context , actorUserID uint64 ) ( [ ] tokencontracts . TokenProduct View, error )
CreateOrder ( ctx context . Context , req tokencontracts . CreateTokenOrderRequest ) ( * gatewaytokenstore . Order View, error )
Lis tOrders ( ctx context . Context , req token contracts. ListToken Orders Request) ( [ ] gatewaytokenstore . OrderView , tokencontracts . PageResult , error )
Ge tOrder( ctx context . Context , actorUserID uint64 , orderID uint64 ) ( * gatewaytokenstore . OrderView , error )
MockPaid Order( ctx context . Context , req tokencontracts . MockPaidOrderRequest ) ( * gatewaytokenstore . OrderView, error )
ListGrants ( ctx context . Context , req token contracts. ListTokenGrantsRequest ) ( [ ] tokencontracts . TokenGrantView , tokencontracts . PageResult , error )
GetCreditBalanceSnapshot ( ctx context . Context , u serID uint64 ) ( * credit contracts. CreditBalanceSnapshot , error )
GetCreditConsumptionDashboard ( ctx context . Context , req creditcontracts . GetCreditConsumptionDashboardRequest ) ( * creditcontracts . CreditConsumptionDashboard View, error )
ListCreditProducts ( ctx context . Context , actorUserID uint64 ) ( [ ] creditcontracts . CreditProduct View, error )
CreateCredi tOrder( ctx context . Context , req credit contracts. CreateCredit OrderRequest) ( * creditcontracts . CreditOrderView , error )
ListCredi tOrders ( ctx context . Context , req creditcontracts . ListCreditOrdersRequest ) ( [ ] creditcontracts . CreditOrderView , creditcontracts . PageResult , error )
GetCredit Order( ctx context . Context , actorUserID uint64 , orderID uint64 ) ( * creditcontracts . Credit OrderView, error )
MockPaidCreditOrder ( ctx context . Context , req credit contracts. MockPaidCreditOrderRequest ) ( * creditcontracts . CreditOrderView , error )
ListCreditTransactions ( ctx context . Context , req creditcontracts . ListCreditTransactionsRequest ) ( [ ] creditcontracts . CreditTransactionView , creditcontracts . PageResult , error )
}
type Handler struct {
@@ -42,53 +57,50 @@ type pageEnvelope[T any] struct {
HasMore bool ` json:"has_more" `
}
type creditSummaryEnvelope struct {
CurrentCreditTotal int64 ` json:"current_credit_total" `
RecordedCreditTotal int64 ` json:"recorded_credit_total" `
AppliedCreditTotal int64 ` json:"applied_credit_total" `
PendingApplyCreditTotal int64 ` json:"pending_apply_credit_total" `
ValidUntil * string ` json:"valid_until" `
QuotaSyncStatus string ` json:"quota_sync_status" `
Tip string ` json:"tip" `
}
type paymentAction struct {
Type string ` json:"type" `
Label string ` json:"label" `
}
type orderCreate Envelope struct {
OrderID uint64 ` json:"order_id" `
OrderNo string ` json:"order_no" `
Status string ` json:"status" `
ProductSnapshot * gatewaytokenstore . ProductSnapshot ` json:"product_snapshot "`
Quantity int ` json:"quantity " `
Token Amount int64 ` json:"token_amou nt" `
AmountCen t int64 ` json:"amount_cen t" `
PriceText string ` json:"price_text " `
Currency string ` json:"currency " `
PaymentMode string ` json:"payment_mod e" `
PaymentAction paymentAction ` json:"payment_action " `
CreatedAt string ` json:"created_at" `
type creditOrder Envelope struct {
OrderID uint64 ` json:"order_id" `
OrderNo string ` json:"order_no" `
Status string ` json:"status" `
Quantity int ` json:"quantity "`
CreditAmount int64 ` json:"credit_amount " `
AmountCent int64 ` json:"amount_ce nt" `
PriceTex t string ` json:"price_tex t" `
Currency string ` json:"currency " `
PaymentMode string ` json:"payment_mode " `
ProductName string ` json:"product_nam e" `
ProductDetail map [ string ] any ` json:"product_snapshot,omitempty " `
CreatedAt string ` json:"created_at" `
PaidAt * string ` json:"paid_at" `
CreditedAt * string ` json:"credited_at" `
PaymentAction paymentAction ` json:"payment_action" `
}
type orderListItem Envelope struct {
Order ID uint64 ` json:"order _id" `
OrderNo string ` json:"order_no " `
Status string ` json:"status " `
ProductName string ` json:"product_name " `
TokenAmount int64 ` json:"token_amount " `
PriceTex t string ` json:"price_tex t" `
CreatedAt string ` json:"created_at " `
PaidAt * string ` json:"paid_at "`
GrantedAt * string ` json:"granted_at " `
}
type orderDetailEnvelope struct {
OrderID uint64 ` json:"order_id" `
OrderNo string ` json:"order_no" `
Status string ` json:"status" `
ProductSnapshot * gatewaytokenstore . ProductSnapshot ` json:"product_snapshot" `
Quantity int ` json:"quantity" `
TokenAmount int64 ` json:"token_amount" `
AmountCent int64 ` json:"amount_cent" `
PriceText string ` json:"price_text" `
Currency string ` json:"currency" `
PaymentMode string ` json:"payment_mode" `
Grant * tokencontracts . TokenGrantView ` json:"grant" `
CreatedAt string ` json:"created_at" `
PaidAt * string ` json:"paid_at" `
GrantedAt * string ` json:"granted_at" `
type creditTransaction Envelope struct {
Grant ID uint64 ` json:"grant _id" `
SourceLabel string ` json:"source_label " `
Amount int64 ` json:"amount " `
Status string ` json:"status " `
Description string ` json:"description " `
CreatedA t string ` json:"created_a t" `
Direction string ` json:"direction " `
BalanceAfter int64 ` json:"balance_after "`
EventID string ` json:"event_id " `
OrderID * uint64 ` json:"order_id" `
}
type createOrderBody struct {
@@ -105,15 +117,36 @@ func (h *Handler) GetSummary(c *gin.Context) {
if ! ok {
return
}
ctx , cancel := context . WithTimeout ( c . Request . Context ( ) , requestTimeout )
defer cancel ( )
summary , err := client . GetSummary ( ctx , currentUserID ( c ) )
snapshot , err := client . GetCreditBalanceSnapshot ( ctx , currentUserID ( c ) )
if err != nil {
respond . DealWithError ( c , err )
return
}
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , summary ) )
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , buildCreditSummaryEnvelope ( snapshot ) ) )
}
func ( h * Handler ) GetConsumptionDashboard ( c * gin . Context ) {
client , ok := h . ready ( c )
if ! ok {
return
}
ctx , cancel := context . WithTimeout ( c . Request . Context ( ) , requestTimeout )
defer cancel ( )
dashboard , err := client . GetCreditConsumptionDashboard ( ctx , creditcontracts . GetCreditConsumptionDashboardRequest {
ActorUserID : currentUserID ( c ) ,
Period : c . Query ( "period" ) ,
} )
if err != nil {
respond . DealWithError ( c , err )
return
}
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , dashboard ) )
}
func ( h * Handler ) ListProducts ( c * gin . Context ) {
@@ -121,10 +154,11 @@ func (h *Handler) ListProducts(c *gin.Context) {
if ! ok {
return
}
ctx , cancel := context . WithTimeout ( c . Request . Context ( ) , requestTimeout )
defer cancel ( )
items , err := client . ListProducts ( ctx , currentUserID ( c ) )
items , err := client . ListCredit Products ( ctx , currentUserID ( c ) )
if err != nil {
respond . DealWithError ( c , err )
return
@@ -137,6 +171,7 @@ func (h *Handler) CreateOrder(c *gin.Context) {
if ! ok {
return
}
var body createOrderBody
if err := c . ShouldBindJSON ( & body ) ; err != nil {
c . JSON ( http . StatusBadRequest , respond . WrongParamType )
@@ -145,7 +180,8 @@ func (h *Handler) CreateOrder(c *gin.Context) {
ctx , cancel := context . WithTimeout ( c . Request . Context ( ) , requestTimeout )
defer cancel ( )
order , err := client . CreateOrder ( ctx , tokencontracts . CreateTokenOrderRequest {
order , err := client . CreateCreditOrder ( ctx , creditcontracts . CreateCreditOrderRequest {
ActorUserID : currentUserID ( c ) ,
ProductID : body . ProductID ,
Quantity : body . Quantity ,
@@ -155,7 +191,7 @@ func (h *Handler) CreateOrder(c *gin.Context) {
respond . DealWithError ( c , err )
return
}
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , newOrderCreate Envelope( order ) ) )
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , buildCreditOrder Envelope( order ) ) )
}
func ( h * Handler ) ListOrders ( c * gin . Context ) {
@@ -163,6 +199,7 @@ func (h *Handler) ListOrders(c *gin.Context) {
if ! ok {
return
}
pageValue , ok := intQuery ( c , "page" )
if ! ok {
return
@@ -174,7 +211,8 @@ func (h *Handler) ListOrders(c *gin.Context) {
ctx , cancel := context . WithTimeout ( c . Request . Context ( ) , requestTimeout )
defer cancel ( )
items , page , err := client . ListOrders ( ctx , tokencontracts . ListTokenOrdersRequest {
items , page , err := client . ListCreditOrders ( ctx , creditcontracts . ListCreditOrdersRequest {
ActorUserID : currentUserID ( c ) ,
Page : pageValue ,
PageSize : pageSize ,
@@ -184,7 +222,13 @@ func (h *Handler) ListOrders(c *gin.Context) {
respond . DealWithError ( c , err )
return
}
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , newPageEnvelope ( newOrderListItemEnvelopes ( items ) , page ) ) )
result := make ( [ ] creditOrderEnvelope , 0 , len ( items ) )
for i := range items {
item := items [ i ]
result = append ( result , buildCreditOrderEnvelope ( & item ) )
}
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , newPageEnvelope ( result , page ) ) )
}
func ( h * Handler ) GetOrder ( c * gin . Context ) {
@@ -192,6 +236,7 @@ func (h *Handler) GetOrder(c *gin.Context) {
if ! ok {
return
}
orderID , ok := uint64Param ( c , "order_id" )
if ! ok {
return
@@ -199,12 +244,13 @@ func (h *Handler) GetOrder(c *gin.Context) {
ctx , cancel := context . WithTimeout ( c . Request . Context ( ) , requestTimeout )
defer cancel ( )
order , err := client . GetOrder ( ctx , currentUserID ( c ) , orderID )
order , err := client . GetCreditOrder ( ctx , currentUserID ( c ) , orderID )
if err != nil {
respond . DealWithError ( c , err )
return
}
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , newOrderDetail Envelope( order ) ) )
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , buildCreditOrder Envelope( order ) ) )
}
func ( h * Handler ) MockPaidOrder ( c * gin . Context ) {
@@ -212,10 +258,12 @@ func (h *Handler) MockPaidOrder(c *gin.Context) {
if ! ok {
return
}
orderID , ok := uint64Param ( c , "order_id" )
if ! ok {
return
}
var body mockPaidBody
if err := c . ShouldBindJSON ( & body ) ; err != nil {
c . JSON ( http . StatusBadRequest , respond . WrongParamType )
@@ -224,24 +272,26 @@ func (h *Handler) MockPaidOrder(c *gin.Context) {
ctx , cancel := context . WithTimeout ( c . Request . Context ( ) , requestTimeout )
defer cancel ( )
order , err := client . MockPaidOrder ( ctx , tokencontracts . MockPaidOrderRequest {
order , err := client . MockPaidCreditOrder ( ctx , creditcontracts . MockPaidCreditOrderRequest {
ActorUserID : currentUserID ( c ) ,
OrderID : orderID ,
MockChannel : body . MockChannel ,
MockChannel : firstNonEmptyString ( strings . TrimSpace ( body . MockChannel ) , "mock" ) ,
IdempotencyKey : strings . TrimSpace ( c . GetHeader ( "X-Idempotency-Key" ) ) ,
} )
if err != nil {
respond . DealWithError ( c , err )
return
}
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , newOrderDetail Envelope( order ) ) )
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , buildCreditOrder Envelope( order ) ) )
}
func ( h * Handler ) ListG rant s ( c * gin . Context ) {
func ( h * Handler ) ListT ransaction s ( c * gin . Context ) {
client , ok := h . ready ( c )
if ! ok {
return
}
pageValue , ok := intQuery ( c , "page" )
if ! ok {
return
@@ -253,22 +303,29 @@ func (h *Handler) ListGrants(c *gin.Context) {
ctx , cancel := context . WithTimeout ( c . Request . Context ( ) , requestTimeout )
defer cancel ( )
items , page , err := client . ListGrants ( ctx , tokencontracts . ListTokenGrantsRequest {
items , page , err := client . ListCreditTransactions ( ctx , creditcontracts . ListCreditTransactionsRequest {
ActorUserID : currentUserID ( c ) ,
Page : pageValue ,
PageSize : pageSize ,
Source : c . Query ( "source" ) ,
Direction : c . Query ( "direction" ) ,
} )
if err != nil {
respond . DealWithError ( c , err )
return
}
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , newPageEnvelope ( items , page ) ) )
result := make ( [ ] creditTransactionEnvelope , 0 , len ( items ) )
for i := range items {
result = append ( result , buildCreditTransactionEnvelope ( items [ i ] ) )
}
c . JSON ( http . StatusOK , respond . RespWithData ( respond . Ok , newPageEnvelope ( result , page ) ) )
}
func ( h * Handler ) ready ( c * gin . Context ) ( TokenStoreClient , bool ) {
if h == nil || h . client == nil {
c . JSON ( http . StatusInternalServerError , respond . InternalError ( errors . New ( "token -store gateway client 未初始化" ) ) )
c . JSON ( http . StatusInternalServerError , respond . InternalError ( errors . New ( "credit -store gateway client 未初始化" ) ) )
return nil , false
}
return h . client , true
@@ -282,82 +339,107 @@ func currentUserID(c *gin.Context) uint64 {
return uint64 ( userID )
}
func newOrderCreateEnvelope ( order * gatewaytokenstore . OrderView ) orderCreateEnvelope {
// buildCreditSummaryEnvelope 负责把权威余额快照转换成钱包摘要返回值。
//
// 职责边界:
// 1. current_credit_total 明确表示“当前可用 Credit 总数”,口径直接复用权威余额。
// 2. 老字段继续保留,避免影响现有前端和兼容逻辑。
// 3. 这里只做展示层拼装,不在网关重复做扣费或账本计算。
func buildCreditSummaryEnvelope ( snapshot * creditcontracts . CreditBalanceSnapshot ) creditSummaryEnvelope {
if snapshot == nil {
return creditSummaryEnvelope {
CurrentCreditTotal : 0 ,
RecordedCreditTotal : 0 ,
AppliedCreditTotal : 0 ,
PendingApplyCreditTotal : 0 ,
ValidUntil : nil ,
QuotaSyncStatus : "synced" ,
Tip : "当前为 Credit 权威账本,购买、奖励和 AI 消费都会实时入账。" ,
}
}
recordedTotal := snapshot . TotalRecharged + snapshot . TotalRewarded
if recordedTotal <= 0 && snapshot . Balance > 0 {
recordedTotal = snapshot . Balance + snapshot . TotalConsumed
}
tip := "当前为 Credit 权威账本,购买、奖励和 AI 消费都会实时入账。"
if snapshot . IsBlocked {
tip = "当前 Credit 余额不足, AI 调用会被阻断;充值后会自动恢复。"
}
return creditSummaryEnvelope {
CurrentCreditTotal : snapshot . Balance ,
RecordedCreditTotal : maxInt64 ( recordedTotal , 0 ) ,
AppliedCreditTotal : snapshot . Balance ,
PendingApplyCreditTotal : 0 ,
ValidUntil : nil ,
QuotaSyncStatus : "synced" ,
Tip : tip ,
}
}
func buildCreditOrderEnvelope ( order * creditcontracts . CreditOrderView ) creditOrderEnvelope {
if order == nil {
return orderCreate Envelope{
return creditOrder Envelope{
PaymentAction : paymentAction {
Type : "mock_paid" ,
Label : "确认支付" ,
} ,
}
}
return orderCreateEnvelope {
OrderID : order . OrderID ,
OrderNo : order . OrderNo ,
Status : order . Status ,
ProductSnapshot : order . ProductSnapshot ,
Quantity : order . Quantity ,
Token Amount: order . Token Amount,
AmountCent : order . AmountCent ,
PriceText : order . PriceText ,
Currency : order . Currency ,
PaymentMode : order . PaymentMode ,
return creditOrderEnvelope {
OrderID : order . OrderID ,
OrderNo : order . OrderNo ,
Status : order . Status ,
Quantity : order . Quantity ,
Credit Amount: order . Credit Amount,
AmountCent : order . AmountCent ,
PriceText : order . PriceText ,
Currency : order . Currency ,
PaymentMode : order . PaymentMode ,
ProductName : order . ProductName ,
ProductDetail : parseJSONMap ( order . ProductSnapshot ) ,
CreatedAt : order . CreatedAt ,
PaidAt : order . PaidAt ,
CreditedAt : order . CreditedAt ,
PaymentAction : paymentAction {
Type : "mock_paid" ,
Label : "确认支付" ,
} ,
CreatedAt : order . CreatedAt ,
}
}
func newOrderListItem Envelopes ( items [ ] gatewaytokenstore . OrderView ) [ ] orderListItem Envelope {
if len ( items ) == 0 {
return [ ] orderListItemEnvelope { }
func buildCreditTransaction Envelope( item creditcontracts . CreditTransactionView ) creditTransaction Envelope {
return creditTransactionEnvelope {
GrantID : item . TransactionID ,
SourceLabel : item . SourceLabel ,
Amount : item . Amount ,
Status : item . Status ,
Description : firstNonEmptyString ( item . Description , item . SourceLabel ) ,
CreatedAt : item . CreatedAt ,
Direction : item . Direction ,
BalanceAfter : item . BalanceAfter ,
EventID : item . EventID ,
OrderID : item . OrderID ,
}
result := make ( [ ] orderListItemEnvelope , 0 , len ( items ) )
for _ , item := range items {
productName := item . ProductName
if productNa me = = "" && item . ProductSnapshot != nil {
productNa me = item . ProductSnapshot . Name
}
result = append ( result , orderListItemEnvelope {
OrderID : item . OrderID ,
OrderNo : item . OrderNo ,
Status : item . Status ,
ProductName : productName ,
TokenAmount : item . TokenAmount ,
PriceText : item . PriceText ,
CreatedAt : item . CreatedAt ,
PaidAt : item . PaidAt ,
GrantedAt : item . GrantedAt ,
} )
}
func parseJSONMap ( raw string ) map [ string ] any {
trim med : = strings . TrimSpace ( raw )
if trim med = = "" {
return nil
}
result := make ( map [ string ] any )
if err := json . Unmarshal ( [ ] byte ( trimmed ) , & result ) ; err != nil {
return nil
}
return result
}
func newOrderDetailEnvelope ( order * gatewaytokenstore . OrderView ) orderDetailEnvelope {
if order == nil {
return orderDetailEnvelope { }
}
return orderDetailEnvelope {
OrderID : order . OrderID ,
OrderNo : order . OrderNo ,
Status : order . Status ,
ProductSnapshot : order . ProductSnapshot ,
Quantity : order . Quantity ,
TokenAmount : order . TokenAmount ,
AmountCent : order . AmountCent ,
PriceText : order . PriceText ,
Currency : order . Currency ,
PaymentMode : order . PaymentMode ,
Grant : order . Grant ,
CreatedAt : order . CreatedAt ,
PaidAt : order . PaidAt ,
GrantedAt : order . GrantedAt ,
}
}
func newPageEnvelope [ T any ] ( items [ ] T , page tokencontracts . PageResult ) pageEnvelope [ T ] {
func newPageEnvelope [ T any ] ( items [ ] T , page creditcontracts . PageResult ) pageEnvelope [ T ] {
return pageEnvelope [ T ] {
Items : items ,
Page : page . Page ,
@@ -372,6 +454,7 @@ func intQuery(c *gin.Context, key string) (int, bool) {
if raw == "" {
return 0 , true
}
value , err := strconv . Atoi ( raw )
if err != nil {
c . JSON ( http . StatusBadRequest , respond . WrongParamType )
@@ -388,3 +471,145 @@ func uint64Param(c *gin.Context, key string) (uint64, bool) {
}
return value , true
}
func firstNonEmptyString ( values ... string ) string {
for _ , value := range values {
if trimmed := strings . TrimSpace ( value ) ; trimmed != "" {
return trimmed
}
}
return ""
}
func maxInt64 ( left int64 , right int64 ) int64 {
if left > right {
return left
}
return right
}
func normalizeCreditConsumptionPeriod ( raw string ) ( string , error ) {
switch strings . TrimSpace ( raw ) {
case "" , creditConsumptionPeriod24h :
return creditConsumptionPeriod24h , nil
case creditConsumptionPeriod7d :
return creditConsumptionPeriod7d , nil
case creditConsumptionPeriod30d :
return creditConsumptionPeriod30d , nil
case creditConsumptionPeriodAll :
return creditConsumptionPeriodAll , nil
default :
return "" , errors . New ( "invalid consumption period" )
}
}
func buildCreditConsumptionDashboard ( ctx context . Context , client TokenStoreClient , actorUserID uint64 , period string ) ( creditcontracts . CreditConsumptionDashboardView , error ) {
startAt , hasWindow := resolveCreditConsumptionWindow ( period , time . Now ( ) )
dashboard := creditcontracts . CreditConsumptionDashboardView {
Period : period ,
}
for page := 1 ; ; page ++ {
items , pageResult , err := client . ListCreditTransactions ( ctx , creditcontracts . ListCreditTransactionsRequest {
ActorUserID : actorUserID ,
Page : page ,
PageSize : creditDashboardPageSize ,
Source : "charge" ,
Direction : "expense" ,
} )
if err != nil {
return creditcontracts . CreditConsumptionDashboardView { } , err
}
if len ( items ) == 0 {
return dashboard , nil
}
for _ , item := range items {
if strings . EqualFold ( strings . TrimSpace ( item . Status ) , "failed" ) {
continue
}
createdAt , ok := parseCreditTransactionCreatedAt ( item . CreatedAt )
if hasWindow && ok && createdAt . Before ( startAt ) {
continue
}
if hasWindow && ! ok {
continue
}
dashboard . CreditConsumed += normalizeCreditConsumedAmount ( item . Amount )
dashboard . TokenConsumed += extractChargeTokenConsumed ( item . MetadataJSON )
}
if ! pageResult . HasMore {
return dashboard , nil
}
if hasWindow && isCreditTransactionPageBeforeWindow ( items , startAt ) {
return dashboard , nil
}
}
}
func resolveCreditConsumptionWindow ( period string , now time . Time ) ( time . Time , bool ) {
switch strings . TrimSpace ( period ) {
case creditConsumptionPeriod24h :
return now . Add ( - 24 * time . Hour ) , true
case creditConsumptionPeriod7d :
return now . Add ( - 7 * 24 * time . Hour ) , true
case creditConsumptionPeriod30d :
return now . Add ( - 30 * 24 * time . Hour ) , true
default :
return time . Time { } , false
}
}
func parseCreditTransactionCreatedAt ( raw string ) ( time . Time , bool ) {
parsed , err := time . Parse ( time . RFC3339 , strings . TrimSpace ( raw ) )
if err != nil {
return time . Time { } , false
}
return parsed , true
}
func isCreditTransactionPageBeforeWindow ( items [ ] creditcontracts . CreditTransactionView , startAt time . Time ) bool {
if len ( items ) == 0 {
return false
}
oldest , ok := parseCreditTransactionCreatedAt ( items [ len ( items ) - 1 ] . CreatedAt )
if ! ok {
return false
}
return oldest . Before ( startAt )
}
func normalizeCreditConsumedAmount ( amount int64 ) int64 {
if amount >= 0 {
return 0
}
return - amount
}
func extractChargeTokenConsumed ( metadataJSON string ) int64 {
if strings . TrimSpace ( metadataJSON ) == "" {
return 0
}
var metadata struct {
InputTokens int64 ` json:"input_tokens" `
OutputTokens int64 ` json:"output_tokens" `
TotalTokens int64 ` json:"total_tokens" `
}
if err := json . Unmarshal ( [ ] byte ( metadataJSON ) , & metadata ) ; err != nil {
return 0
}
if metadata . TotalTokens > 0 {
return metadata . TotalTokens
}
total := metadata . InputTokens + metadata . OutputTokens
if total < 0 {
return 0
}
return total
}