+source currency pick fix +fx side propagation

This commit is contained in:
Stephan D
2026-02-26 02:39:48 +01:00
parent 008427483c
commit 70b1c2a9cc
73 changed files with 2123 additions and 656 deletions

View File

@@ -12,20 +12,13 @@ type PaymentQuoteRecord struct {
storable.Base `bson:",inline" json:",inline"`
model.OrganizationBoundBase `bson:",inline" json:",inline"`
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
Intent PaymentIntent `bson:"intent,omitempty" json:"intent,omitempty"`
Intents []PaymentIntent `bson:"intents,omitempty" json:"intents,omitempty"`
Quote *PaymentQuoteSnapshot `bson:"quote,omitempty" json:"quote,omitempty"`
Quotes []*PaymentQuoteSnapshot `bson:"quotes,omitempty" json:"quotes,omitempty"`
StatusV2 *QuoteStatusV2 `bson:"statusV2,omitempty" json:"statusV2,omitempty"`
StatusesV2 []*QuoteStatusV2 `bson:"statusesV2,omitempty" json:"statusesV2,omitempty"`
Plan *PaymentPlan `bson:"plan,omitempty" json:"plan,omitempty"`
Plans []*PaymentPlan `bson:"plans,omitempty" json:"plans,omitempty"`
ExecutionNote string `bson:"executionNote,omitempty" json:"executionNote,omitempty"`
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
PurgeAt time.Time `bson:"purgeAt,omitempty" json:"purgeAt,omitempty"`
Hash string `bson:"hash" json:"hash"`
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
RequestShape QuoteRequestShape `bson:"requestShape,omitempty" json:"requestShape,omitempty"`
Items []*PaymentQuoteItemV2 `bson:"items,omitempty" json:"items,omitempty"`
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
PurgeAt time.Time `bson:"purgeAt,omitempty" json:"purgeAt,omitempty"`
Hash string `bson:"hash" json:"hash"`
}
// Collection implements storable.Storable.

View File

@@ -1,5 +1,14 @@
package model
// QuoteRequestShape identifies the API surface that created the quote record.
type QuoteRequestShape string
const (
QuoteRequestShapeUnspecified QuoteRequestShape = "unspecified"
QuoteRequestShapeSingle QuoteRequestShape = "single"
QuoteRequestShapeBatch QuoteRequestShape = "batch"
)
// QuoteState captures v2 quote state metadata for persistence.
type QuoteState string
@@ -30,3 +39,10 @@ type QuoteStatusV2 struct {
State QuoteState `bson:"state,omitempty" json:"state,omitempty"`
BlockReason QuoteBlockReason `bson:"blockReason,omitempty" json:"blockReason,omitempty"`
}
// PaymentQuoteItemV2 keeps one intent/quote/status tuple in a stable shape.
type PaymentQuoteItemV2 struct {
Intent *PaymentIntent `bson:"intent,omitempty" json:"intent,omitempty"`
Quote *PaymentQuoteSnapshot `bson:"quote,omitempty" json:"quote,omitempty"`
Status *QuoteStatusV2 `bson:"status,omitempty" json:"status,omitempty"`
}

View File

@@ -3,6 +3,7 @@ package store
import (
"context"
"errors"
"strconv"
"strings"
"time"
@@ -90,25 +91,39 @@ func (q *Quotes) Create(ctx context.Context, quote *model.PaymentQuoteRecord) er
if quote.IdempotencyKey == "" {
return merrors.InvalidArgument("quotesStore: idempotency key is required")
}
quote.ExecutionNote = strings.TrimSpace(quote.ExecutionNote)
quote.RequestShape = model.QuoteRequestShape(strings.TrimSpace(string(quote.RequestShape)))
if quote.RequestShape == "" || quote.RequestShape == model.QuoteRequestShapeUnspecified {
return merrors.InvalidArgument("quotesStore: request shape is required")
}
if len(quote.Items) == 0 {
return merrors.InvalidArgument("quotesStore: items are required")
}
if quote.RequestShape == model.QuoteRequestShapeSingle && len(quote.Items) != 1 {
return merrors.InvalidArgument("quotesStore: single shape requires exactly one item")
}
if quote.ExpiresAt.IsZero() {
return merrors.InvalidArgument("quotesStore: expires_at is required")
}
if quote.PurgeAt.IsZero() || quote.PurgeAt.Before(quote.ExpiresAt) {
quote.PurgeAt = quote.ExpiresAt.Add(q.retention)
}
if quote.Intent.Attributes != nil {
for k, v := range quote.Intent.Attributes {
quote.Intent.Attributes[k] = strings.TrimSpace(v)
for i := range quote.Items {
item := quote.Items[i]
if item == nil {
return merrors.InvalidArgument("quotesStore: items[" + strconv.Itoa(i) + "] is required")
}
}
if len(quote.Intents) > 0 {
for i := range quote.Intents {
if quote.Intents[i].Attributes == nil {
continue
}
for k, v := range quote.Intents[i].Attributes {
quote.Intents[i].Attributes[k] = strings.TrimSpace(v)
if item.Intent == nil {
return merrors.InvalidArgument("quotesStore: items[" + strconv.Itoa(i) + "].intent is required")
}
if item.Quote == nil {
return merrors.InvalidArgument("quotesStore: items[" + strconv.Itoa(i) + "].quote is required")
}
if item.Status == nil {
return merrors.InvalidArgument("quotesStore: items[" + strconv.Itoa(i) + "].status is required")
}
if item.Intent.Attributes != nil {
for k, v := range item.Intent.Attributes {
item.Intent.Attributes[k] = strings.TrimSpace(v)
}
}
}