double-sided quotation + fixed tests
This commit is contained in:
@@ -29,7 +29,7 @@ require (
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
|
||||
github.com/casbin/casbin/v2 v2.134.0 // indirect
|
||||
github.com/casbin/casbin/v2 v2.135.0 // indirect
|
||||
github.com/casbin/govaluate v1.10.0 // indirect
|
||||
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
|
||||
@@ -9,8 +9,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
|
||||
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
|
||||
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
|
||||
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
|
||||
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
|
||||
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||
@@ -22,12 +24,38 @@ import (
|
||||
func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.PaymentQuote, time.Time, error) {
|
||||
intent := req.GetIntent()
|
||||
amount := intent.GetAmount()
|
||||
baseAmount := cloneMoney(amount)
|
||||
feeQuote, err := s.quoteFees(ctx, orgRef, req)
|
||||
fxSide := fxv1.Side_SIDE_UNSPECIFIED
|
||||
if intent.GetFx() != nil {
|
||||
fxSide = intent.GetFx().GetSide()
|
||||
}
|
||||
|
||||
var fxQuote *oraclev1.Quote
|
||||
var err error
|
||||
if shouldRequestFX(intent) {
|
||||
fxQuote, err = s.requestFXQuote(ctx, orgRef, req)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
}
|
||||
|
||||
payAmount, settlementAmountBeforeFees := resolveTradeAmounts(amount, fxQuote, fxSide)
|
||||
|
||||
feeBaseAmount := payAmount
|
||||
if feeBaseAmount == nil {
|
||||
feeBaseAmount = cloneMoney(amount)
|
||||
}
|
||||
|
||||
feeQuote, err := s.quoteFees(ctx, orgRef, req, feeBaseAmount)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
feeTotal := extractFeeTotal(feeQuote.GetLines(), amount.GetCurrency())
|
||||
feeCurrency := ""
|
||||
if feeBaseAmount != nil {
|
||||
feeCurrency = feeBaseAmount.GetCurrency()
|
||||
} else if amount != nil {
|
||||
feeCurrency = amount.GetCurrency()
|
||||
}
|
||||
feeTotal := extractFeeTotal(feeQuote.GetLines(), feeCurrency)
|
||||
|
||||
var networkFee *chainv1.EstimateTransferFeeResponse
|
||||
if shouldEstimateNetworkFee(intent) {
|
||||
@@ -37,15 +65,7 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
|
||||
}
|
||||
}
|
||||
|
||||
var fxQuote *oraclev1.Quote
|
||||
if shouldRequestFX(intent) {
|
||||
fxQuote, err = s.requestFXQuote(ctx, orgRef, req)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
}
|
||||
|
||||
debitAmount, settlementAmount := computeAggregates(baseAmount, feeTotal, networkFee)
|
||||
debitAmount, settlementAmount := computeAggregates(payAmount, settlementAmountBeforeFees, feeTotal, networkFee, fxQuote)
|
||||
|
||||
quote := &orchestratorv1.PaymentQuote{
|
||||
DebitAmount: debitAmount,
|
||||
@@ -63,14 +83,18 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
|
||||
return quote, expiresAt, nil
|
||||
}
|
||||
|
||||
func (s *Service) quoteFees(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest) (*feesv1.PrecomputeFeesResponse, error) {
|
||||
func (s *Service) quoteFees(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest, baseAmount *moneyv1.Money) (*feesv1.PrecomputeFeesResponse, error) {
|
||||
if !s.fees.available() {
|
||||
return &feesv1.PrecomputeFeesResponse{}, nil
|
||||
}
|
||||
intent := req.GetIntent()
|
||||
amount := cloneMoney(baseAmount)
|
||||
if amount == nil {
|
||||
amount = cloneMoney(intent.GetAmount())
|
||||
}
|
||||
feeIntent := &feesv1.Intent{
|
||||
Trigger: triggerFromKind(intent.GetKind(), intent.GetRequiresFx()),
|
||||
BaseAmount: cloneMoney(intent.GetAmount()),
|
||||
BaseAmount: amount,
|
||||
BookedAt: timestamppb.New(s.clock.Now()),
|
||||
OriginType: "payments.orchestrator.quote",
|
||||
OriginRef: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||
@@ -164,7 +188,19 @@ func (s *Service) requestFXQuote(ctx context.Context, orgRef string, req *orches
|
||||
}
|
||||
|
||||
if amount := intent.GetAmount(); amount != nil {
|
||||
params.BaseAmount = cloneMoney(amount)
|
||||
pair := fxIntent.GetPair()
|
||||
if pair != nil {
|
||||
switch {
|
||||
case strings.EqualFold(amount.GetCurrency(), pair.GetBase()):
|
||||
params.BaseAmount = cloneMoney(amount)
|
||||
case strings.EqualFold(amount.GetCurrency(), pair.GetQuote()):
|
||||
params.QuoteAmount = cloneMoney(amount)
|
||||
default:
|
||||
params.BaseAmount = cloneMoney(amount)
|
||||
}
|
||||
} else {
|
||||
params.BaseAmount = cloneMoney(amount)
|
||||
}
|
||||
}
|
||||
|
||||
quote, err := s.oracle.client.GetQuote(ctx, params)
|
||||
@@ -291,11 +327,14 @@ func (s *Service) applyFX(ctx context.Context, payment *model.Payment, quote *or
|
||||
if fq == nil {
|
||||
return merrors.InvalidArgument("ledger: fx quote missing")
|
||||
}
|
||||
fromMoney := cloneMoney(fq.GetBaseAmount())
|
||||
fxSide := fxv1.Side_SIDE_UNSPECIFIED
|
||||
if intent.FX != nil {
|
||||
fxSide = intent.FX.Side
|
||||
}
|
||||
fromMoney, toMoney := resolveTradeAmounts(intent.Amount, fq, fxSide)
|
||||
if fromMoney == nil {
|
||||
fromMoney = cloneMoney(intent.Amount)
|
||||
}
|
||||
toMoney := cloneMoney(fq.GetQuoteAmount())
|
||||
if toMoney == nil {
|
||||
toMoney = cloneMoney(quote.GetExpectedSettlementAmount())
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
|
||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
)
|
||||
@@ -109,30 +110,91 @@ func extractFeeTotal(lines []*feesv1.DerivedPostingLine, currency string) *money
|
||||
}
|
||||
}
|
||||
|
||||
func computeAggregates(base, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse) (*moneyv1.Money, *moneyv1.Money) {
|
||||
if base == nil {
|
||||
func resolveTradeAmounts(intentAmount *moneyv1.Money, fxQuote *oraclev1.Quote, side fxv1.Side) (*moneyv1.Money, *moneyv1.Money) {
|
||||
if fxQuote == nil {
|
||||
return cloneMoney(intentAmount), cloneMoney(intentAmount)
|
||||
}
|
||||
qSide := fxQuote.GetSide()
|
||||
if qSide == fxv1.Side_SIDE_UNSPECIFIED {
|
||||
qSide = side
|
||||
}
|
||||
|
||||
switch qSide {
|
||||
case fxv1.Side_BUY_BASE_SELL_QUOTE:
|
||||
pay := cloneMoney(fxQuote.GetQuoteAmount())
|
||||
settle := cloneMoney(fxQuote.GetBaseAmount())
|
||||
if pay == nil {
|
||||
pay = cloneMoney(intentAmount)
|
||||
}
|
||||
if settle == nil {
|
||||
settle = cloneMoney(intentAmount)
|
||||
}
|
||||
return pay, settle
|
||||
case fxv1.Side_SELL_BASE_BUY_QUOTE:
|
||||
pay := cloneMoney(fxQuote.GetBaseAmount())
|
||||
settle := cloneMoney(fxQuote.GetQuoteAmount())
|
||||
if pay == nil {
|
||||
pay = cloneMoney(intentAmount)
|
||||
}
|
||||
if settle == nil {
|
||||
settle = cloneMoney(intentAmount)
|
||||
}
|
||||
return pay, settle
|
||||
default:
|
||||
return cloneMoney(intentAmount), cloneMoney(intentAmount)
|
||||
}
|
||||
}
|
||||
|
||||
func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse, fxQuote *oraclev1.Quote) (*moneyv1.Money, *moneyv1.Money) {
|
||||
if pay == nil {
|
||||
return nil, nil
|
||||
}
|
||||
baseDecimal, err := decimalFromMoney(base)
|
||||
debitDecimal, err := decimalFromMoney(pay)
|
||||
if err != nil {
|
||||
return cloneMoney(base), cloneMoney(base)
|
||||
}
|
||||
debit := baseDecimal
|
||||
settlement := baseDecimal
|
||||
|
||||
if feeDecimal, err := decimalFromMoneyMatching(base, fee); err == nil && feeDecimal != nil {
|
||||
debit = debit.Add(*feeDecimal)
|
||||
settlement = settlement.Sub(*feeDecimal)
|
||||
return cloneMoney(pay), cloneMoney(settlement)
|
||||
}
|
||||
|
||||
if network != nil && network.GetNetworkFee() != nil {
|
||||
if networkDecimal, err := decimalFromMoneyMatching(base, network.GetNetworkFee()); err == nil && networkDecimal != nil {
|
||||
debit = debit.Add(*networkDecimal)
|
||||
settlement = settlement.Sub(*networkDecimal)
|
||||
settlementCurrency := pay.GetCurrency()
|
||||
if settlement != nil && strings.TrimSpace(settlement.GetCurrency()) != "" {
|
||||
settlementCurrency = settlement.GetCurrency()
|
||||
}
|
||||
|
||||
settlementDecimal := debitDecimal
|
||||
if settlement != nil {
|
||||
if val, err := decimalFromMoney(settlement); err == nil {
|
||||
settlementDecimal = val
|
||||
}
|
||||
}
|
||||
|
||||
return makeMoney(base.GetCurrency(), debit), makeMoney(base.GetCurrency(), settlement)
|
||||
adjustDebit := func(m *moneyv1.Money) {
|
||||
converted, err := ensureCurrency(m, pay.GetCurrency(), fxQuote)
|
||||
if err != nil || converted == nil {
|
||||
return
|
||||
}
|
||||
if val, err := decimalFromMoney(converted); err == nil {
|
||||
debitDecimal = debitDecimal.Add(val)
|
||||
}
|
||||
}
|
||||
|
||||
adjustSettlement := func(m *moneyv1.Money) {
|
||||
converted, err := ensureCurrency(m, settlementCurrency, fxQuote)
|
||||
if err != nil || converted == nil {
|
||||
return
|
||||
}
|
||||
if val, err := decimalFromMoney(converted); err == nil {
|
||||
settlementDecimal = settlementDecimal.Sub(val)
|
||||
}
|
||||
}
|
||||
|
||||
adjustDebit(fee)
|
||||
adjustSettlement(fee)
|
||||
|
||||
if network != nil && network.GetNetworkFee() != nil {
|
||||
adjustDebit(network.GetNetworkFee())
|
||||
adjustSettlement(network.GetNetworkFee())
|
||||
}
|
||||
|
||||
return makeMoney(pay.GetCurrency(), debitDecimal), makeMoney(settlementCurrency, settlementDecimal)
|
||||
}
|
||||
|
||||
func decimalFromMoney(m *moneyv1.Money) (decimal.Decimal, error) {
|
||||
@@ -163,6 +225,46 @@ func makeMoney(currency string, value decimal.Decimal) *moneyv1.Money {
|
||||
}
|
||||
}
|
||||
|
||||
func ensureCurrency(m *moneyv1.Money, targetCurrency string, quote *oraclev1.Quote) (*moneyv1.Money, error) {
|
||||
if m == nil || strings.TrimSpace(targetCurrency) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if strings.EqualFold(m.GetCurrency(), targetCurrency) {
|
||||
return cloneMoney(m), nil
|
||||
}
|
||||
return convertWithQuote(m, quote, targetCurrency)
|
||||
}
|
||||
|
||||
func convertWithQuote(m *moneyv1.Money, quote *oraclev1.Quote, targetCurrency string) (*moneyv1.Money, error) {
|
||||
if m == nil || quote == nil || quote.GetPair() == nil || quote.GetPrice() == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
base := strings.TrimSpace(quote.GetPair().GetBase())
|
||||
qt := strings.TrimSpace(quote.GetPair().GetQuote())
|
||||
if base == "" || qt == "" || strings.TrimSpace(targetCurrency) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
price, err := decimal.NewFromString(quote.GetPrice().GetValue())
|
||||
if err != nil || price.IsZero() {
|
||||
return nil, err
|
||||
}
|
||||
value, err := decimalFromMoney(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.EqualFold(m.GetCurrency(), base) && strings.EqualFold(targetCurrency, qt):
|
||||
return makeMoney(targetCurrency, value.Mul(price)), nil
|
||||
case strings.EqualFold(m.GetCurrency(), qt) && strings.EqualFold(targetCurrency, base):
|
||||
return makeMoney(targetCurrency, value.Div(price)), nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func quoteToProto(src *oracleclient.Quote) *oraclev1.Quote {
|
||||
if src == nil {
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||
)
|
||||
|
||||
func TestResolveTradeAmountsBuyBase(t *testing.T) {
|
||||
fxQuote := &oraclev1.Quote{
|
||||
Side: fxv1.Side_BUY_BASE_SELL_QUOTE,
|
||||
Pair: &fxv1.CurrencyPair{Base: "EUR", Quote: "USD"},
|
||||
BaseAmount: &moneyv1.Money{
|
||||
Currency: "EUR",
|
||||
Amount: "100",
|
||||
},
|
||||
QuoteAmount: &moneyv1.Money{
|
||||
Currency: "USD",
|
||||
Amount: "110",
|
||||
},
|
||||
}
|
||||
|
||||
pay, settle := resolveTradeAmounts(nil, fxQuote, fxv1.Side_SIDE_UNSPECIFIED)
|
||||
if pay.GetCurrency() != "USD" || pay.GetAmount() != "110" {
|
||||
t.Fatalf("expected pay amount in USD 110, got %s %s", pay.GetCurrency(), pay.GetAmount())
|
||||
}
|
||||
if settle.GetCurrency() != "EUR" || settle.GetAmount() != "100" {
|
||||
t.Fatalf("expected settlement in EUR 100, got %s %s", settle.GetCurrency(), settle.GetAmount())
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeAggregatesConvertsCurrencies(t *testing.T) {
|
||||
pay := &moneyv1.Money{Currency: "USD", Amount: "100"}
|
||||
settle := &moneyv1.Money{Currency: "EUR", Amount: "50"}
|
||||
fee := &moneyv1.Money{Currency: "USD", Amount: "10"}
|
||||
network := &chainv1.EstimateTransferFeeResponse{
|
||||
NetworkFee: &moneyv1.Money{Currency: "USD", Amount: "5"},
|
||||
}
|
||||
fxQuote := &oraclev1.Quote{
|
||||
Pair: &fxv1.CurrencyPair{Base: "EUR", Quote: "USD"},
|
||||
Side: fxv1.Side_BUY_BASE_SELL_QUOTE,
|
||||
Price: &moneyv1.Decimal{
|
||||
Value: "2",
|
||||
},
|
||||
}
|
||||
|
||||
debit, settlement := computeAggregates(pay, settle, fee, network, fxQuote)
|
||||
if debit.GetCurrency() != "USD" || debit.GetAmount() != "115" {
|
||||
t.Fatalf("expected debit 115 USD, got %s %s", debit.GetCurrency(), debit.GetAmount())
|
||||
}
|
||||
if settlement.GetCurrency() != "EUR" || settlement.GetAmount() != "42.5" {
|
||||
t.Fatalf("expected settlement 42.5 EUR, got %s %s", settlement.GetCurrency(), settlement.GetAmount())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestRequestFXQuoteUsesQuoteAmountWhenCurrencyMatchesQuote(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var captured oracleclient.GetQuoteParams
|
||||
|
||||
svc := &Service{
|
||||
logger: zap.NewNop(),
|
||||
clock: testClock{now: time.Now()},
|
||||
oracle: oracleDependency{
|
||||
client: &oracleclient.Fake{
|
||||
GetQuoteFn: func(ctx context.Context, params oracleclient.GetQuoteParams) (*oracleclient.Quote, error) {
|
||||
captured = params
|
||||
return &oracleclient.Quote{
|
||||
QuoteRef: "q",
|
||||
Pair: params.Pair,
|
||||
Side: params.Side,
|
||||
Price: "1.1",
|
||||
BaseAmount: params.BaseAmount,
|
||||
QuoteAmount: params.QuoteAmount,
|
||||
ExpiresAt: time.Now(),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req := &orchestratorv1.QuotePaymentRequest{
|
||||
Meta: &orchestratorv1.RequestMeta{OrganizationRef: "org"},
|
||||
Intent: &orchestratorv1.PaymentIntent{
|
||||
Amount: &moneyv1.Money{Currency: "USD", Amount: "100"},
|
||||
Fx: &orchestratorv1.FXIntent{
|
||||
Pair: &fxv1.CurrencyPair{Base: "EUR", Quote: "USD"},
|
||||
Side: fxv1.Side_BUY_BASE_SELL_QUOTE,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := svc.requestFXQuote(ctx, "org", req); err != nil {
|
||||
t.Fatalf("requestFXQuote returned error: %v", err)
|
||||
}
|
||||
|
||||
if captured.QuoteAmount == nil {
|
||||
t.Fatal("expected quote amount to be populated")
|
||||
}
|
||||
if captured.BaseAmount != nil {
|
||||
t.Fatal("expected base amount to be nil when using quote amount input")
|
||||
}
|
||||
if captured.QuoteAmount.GetCurrency() != "USD" {
|
||||
t.Fatalf("expected quote amount currency USD, got %s", captured.QuoteAmount.GetCurrency())
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,8 @@ func NewQuotes(logger mlogger.Logger, repo repository.Repository) (*Quotes, erro
|
||||
Keys: []ri.Key{{Field: "organizationRef", Sort: ri.Asc}},
|
||||
},
|
||||
{
|
||||
Keys: []ri.Key{{Field: "expiresAt", Sort: ri.Asc}},
|
||||
ExpireAfterSeconds: 0,
|
||||
Keys: []ri.Key{{Field: "expiresAt", Sort: ri.Asc}},
|
||||
TTL: int32Ptr(0),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -111,3 +111,7 @@ func (q *Quotes) GetByRef(ctx context.Context, orgRef primitive.ObjectID, quoteR
|
||||
}
|
||||
|
||||
var _ storage.QuotesStore = (*Quotes)(nil)
|
||||
|
||||
func int32Ptr(v int32) *int32 {
|
||||
return &v
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user