move api/server to api/edge/bff

This commit is contained in:
Stephan D
2026-02-28 00:39:20 +01:00
parent 34182af3b8
commit 98db0e4e9e
248 changed files with 406 additions and 18 deletions

View File

@@ -0,0 +1,389 @@
package sresponse
import (
"net/http"
"strconv"
"strings"
"time"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
"google.golang.org/protobuf/types/known/timestamppb"
)
type FeeLine struct {
LedgerAccountRef string `json:"ledgerAccountRef,omitempty"`
Amount *paymenttypes.Money `json:"amount,omitempty"`
LineType string `json:"lineType,omitempty"`
Side string `json:"side,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
}
type FxQuote struct {
QuoteRef string `json:"quoteRef,omitempty"`
BaseCurrency string `json:"baseCurrency,omitempty"`
QuoteCurrency string `json:"quoteCurrency,omitempty"`
Side string `json:"side,omitempty"`
Price string `json:"price,omitempty"`
BaseAmount *paymenttypes.Money `json:"baseAmount,omitempty"`
QuoteAmount *paymenttypes.Money `json:"quoteAmount,omitempty"`
ExpiresAtUnixMs int64 `json:"expiresAtUnixMs,omitempty"`
PricedAtUnixMs int64 `json:"pricedAtUnixMs,omitempty"`
Provider string `json:"provider,omitempty"`
RateRef string `json:"rateRef,omitempty"`
Firm bool `json:"firm,omitempty"`
}
type PaymentQuote struct {
QuoteRef string `json:"quoteRef,omitempty"`
IntentRef string `json:"intentRef,omitempty"`
Amounts *QuoteAmounts `json:"amounts,omitempty"`
Fees *QuoteFees `json:"fees,omitempty"`
FxQuote *FxQuote `json:"fxQuote,omitempty"`
}
type QuoteAmounts struct {
SourcePrincipal *paymenttypes.Money `json:"sourcePrincipal,omitempty"`
SourceDebitTotal *paymenttypes.Money `json:"sourceDebitTotal,omitempty"`
DestinationSettlement *paymenttypes.Money `json:"destinationSettlement,omitempty"`
}
type QuoteFees struct {
Lines []FeeLine `json:"lines,omitempty"`
}
type PaymentQuotes struct {
IdempotencyKey string `json:"idempotencyKey,omitempty"`
QuoteRef string `json:"quoteRef,omitempty"`
Items []PaymentQuote `json:"items,omitempty"`
}
type Payment struct {
PaymentRef string `json:"paymentRef,omitempty"`
IdempotencyKey string `json:"idempotencyKey,omitempty"`
State string `json:"state,omitempty"`
FailureCode string `json:"failureCode,omitempty"`
FailureReason string `json:"failureReason,omitempty"`
Operations []PaymentOperation `json:"operations,omitempty"`
LastQuote *PaymentQuote `json:"lastQuote,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
}
type PaymentOperation struct {
StepRef string `json:"stepRef,omitempty"`
Code string `json:"code,omitempty"`
State string `json:"state,omitempty"`
Label string `json:"label,omitempty"`
FailureCode string `json:"failureCode,omitempty"`
FailureReason string `json:"failureReason,omitempty"`
StartedAt time.Time `json:"startedAt,omitempty"`
CompletedAt time.Time `json:"completedAt,omitempty"`
}
type paymentQuoteResponse struct {
authResponse `json:",inline"`
IdempotencyKey string `json:"idempotencyKey,omitempty"`
Quote *PaymentQuote `json:"quote"`
}
type paymentQuotesResponse struct {
authResponse `json:",inline"`
Quote *PaymentQuotes `json:"quote"`
}
type paymentsResponse struct {
authResponse `json:",inline"`
Payments []Payment `json:"payments"`
Page *paginationv1.CursorPageResponse `json:"page,omitempty"`
}
type paymentResponse struct {
authResponse `json:",inline"`
Payment *Payment `json:"payment"`
}
// PaymentQuote wraps a payment quote with refreshed access token.
func PaymentQuoteResponse(logger mlogger.Logger, idempotencyKey string, quote *quotationv2.PaymentQuote, token *TokenData) http.HandlerFunc {
return response.Ok(logger, paymentQuoteResponse{
Quote: toPaymentQuote(quote),
IdempotencyKey: idempotencyKey,
authResponse: authResponse{AccessToken: *token},
})
}
// PaymentQuotes wraps batch quotes with refreshed access token.
func PaymentQuotesResponse(logger mlogger.Logger, resp *quotationv2.QuotePaymentsResponse, token *TokenData) http.HandlerFunc {
return response.Ok(logger, paymentQuotesResponse{
Quote: toPaymentQuotes(resp),
authResponse: authResponse{AccessToken: *token},
})
}
// Payments wraps a list of payments with refreshed access token.
func PaymentsResponse(logger mlogger.Logger, payments []*orchestrationv2.Payment, token *TokenData) http.HandlerFunc {
return response.Ok(logger, paymentsResponse{
Payments: toPayments(payments),
authResponse: authResponse{AccessToken: *token},
})
}
// PaymentsList wraps a list of payments with refreshed access token and pagination data.
func PaymentsListResponse(logger mlogger.Logger, resp *orchestrationv2.ListPaymentsResponse, token *TokenData) http.HandlerFunc {
return response.Ok(logger, paymentsResponse{
Payments: toPayments(resp.GetPayments()),
Page: resp.GetPage(),
authResponse: authResponse{AccessToken: *token},
})
}
// Payment wraps a payment with refreshed access token.
func PaymentResponse(logger mlogger.Logger, payment *orchestrationv2.Payment, token *TokenData) http.HandlerFunc {
return response.Ok(logger, paymentResponse{
Payment: toPayment(payment),
authResponse: authResponse{AccessToken: *token},
})
}
func toFeeLines(lines []*feesv1.DerivedPostingLine) []FeeLine {
if len(lines) == 0 {
return nil
}
result := make([]FeeLine, 0, len(lines))
for _, line := range lines {
if line == nil {
continue
}
result = append(result, FeeLine{
LedgerAccountRef: line.GetLedgerAccountRef(),
Amount: toMoney(line.GetMoney()),
LineType: enumJSONName(line.GetLineType().String()),
Side: enumJSONName(line.GetSide().String()),
Meta: line.GetMeta(),
})
}
if len(result) == 0 {
return nil
}
return result
}
func toFxQuote(q *oraclev1.Quote) *FxQuote {
if q == nil {
return nil
}
pair := q.GetPair()
pricedAtUnixMs := int64(0)
if ts := q.GetPricedAt(); ts != nil {
pricedAtUnixMs = ts.AsTime().UnixMilli()
}
base := ""
quote := ""
if pair != nil {
base = pair.GetBase()
quote = pair.GetQuote()
}
return &FxQuote{
QuoteRef: q.GetQuoteRef(),
BaseCurrency: base,
QuoteCurrency: quote,
Side: enumJSONName(q.GetSide().String()),
Price: q.GetPrice().GetValue(),
BaseAmount: toMoney(q.GetBaseAmount()),
QuoteAmount: toMoney(q.GetQuoteAmount()),
ExpiresAtUnixMs: q.GetExpiresAtUnixMs(),
PricedAtUnixMs: pricedAtUnixMs,
Provider: q.GetProvider(),
RateRef: q.GetRateRef(),
Firm: q.GetFirm(),
}
}
func toPaymentQuote(q *quotationv2.PaymentQuote) *PaymentQuote {
if q == nil {
return nil
}
amounts := toQuoteAmounts(q)
fees := toQuoteFees(q.GetFeeLines())
return &PaymentQuote{
QuoteRef: q.GetQuoteRef(),
IntentRef: strings.TrimSpace(q.GetIntentRef()),
Amounts: amounts,
Fees: fees,
FxQuote: toFxQuote(q.GetFxQuote()),
}
}
func toPaymentQuotes(resp *quotationv2.QuotePaymentsResponse) *PaymentQuotes {
if resp == nil {
return nil
}
items := make([]PaymentQuote, 0, len(resp.GetQuotes()))
for _, quote := range resp.GetQuotes() {
if dto := toPaymentQuote(quote); dto != nil {
items = append(items, *dto)
}
}
if len(items) == 0 {
items = nil
}
return &PaymentQuotes{
IdempotencyKey: resp.GetIdempotencyKey(),
QuoteRef: resp.GetQuoteRef(),
Items: items,
}
}
func toQuoteAmounts(q *quotationv2.PaymentQuote) *QuoteAmounts {
if q == nil {
return nil
}
amounts := &QuoteAmounts{
SourcePrincipal: toMoney(q.GetTransferPrincipalAmount()),
SourceDebitTotal: toMoney(q.GetPayerTotalDebitAmount()),
DestinationSettlement: toMoney(q.GetDestinationAmount()),
}
if amounts.SourcePrincipal == nil && amounts.SourceDebitTotal == nil && amounts.DestinationSettlement == nil {
return nil
}
return amounts
}
func toQuoteFees(lines []*feesv1.DerivedPostingLine) *QuoteFees {
feeLines := toFeeLines(lines)
if len(feeLines) == 0 {
return nil
}
return &QuoteFees{Lines: feeLines}
}
func toPayments(items []*orchestrationv2.Payment) []Payment {
if len(items) == 0 {
return nil
}
result := make([]Payment, 0, len(items))
for _, item := range items {
if p := toPayment(item); p != nil {
result = append(result, *p)
}
}
if len(result) == 0 {
return nil
}
return result
}
func toPayment(p *orchestrationv2.Payment) *Payment {
if p == nil {
return nil
}
operations := toUserVisibleOperations(p.GetStepExecutions())
failureCode, failureReason := firstFailure(operations)
return &Payment{
PaymentRef: p.GetPaymentRef(),
State: enumJSONName(p.GetState().String()),
FailureCode: failureCode,
FailureReason: failureReason,
Operations: operations,
LastQuote: toPaymentQuote(p.GetQuoteSnapshot()),
CreatedAt: timestampAsTime(p.GetCreatedAt()),
Meta: paymentMeta(p),
IdempotencyKey: "",
}
}
func firstFailure(operations []PaymentOperation) (string, string) {
for _, op := range operations {
if strings.TrimSpace(op.FailureCode) == "" && strings.TrimSpace(op.FailureReason) == "" {
continue
}
return strings.TrimSpace(op.FailureCode), strings.TrimSpace(op.FailureReason)
}
return "", ""
}
func toUserVisibleOperations(steps []*orchestrationv2.StepExecution) []PaymentOperation {
if len(steps) == 0 {
return nil
}
ops := make([]PaymentOperation, 0, len(steps))
for _, step := range steps {
if step == nil || !isUserVisibleStep(step.GetReportVisibility()) {
continue
}
ops = append(ops, toPaymentOperation(step))
}
if len(ops) == 0 {
return nil
}
return ops
}
func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation {
op := PaymentOperation{
StepRef: step.GetStepRef(),
Code: step.GetStepCode(),
State: enumJSONName(step.GetState().String()),
Label: strings.TrimSpace(step.GetUserLabel()),
StartedAt: timestampAsTime(step.GetStartedAt()),
CompletedAt: timestampAsTime(step.GetCompletedAt()),
}
failure := step.GetFailure()
if failure == nil {
return op
}
op.FailureCode = enumJSONName(failure.GetCategory().String())
op.FailureReason = strings.TrimSpace(failure.GetMessage())
if op.FailureReason == "" {
op.FailureReason = strings.TrimSpace(failure.GetCode())
}
return op
}
func isUserVisibleStep(visibility orchestrationv2.ReportVisibility) bool {
switch visibility {
case orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN,
orchestrationv2.ReportVisibility_REPORT_VISIBILITY_BACKOFFICE,
orchestrationv2.ReportVisibility_REPORT_VISIBILITY_AUDIT:
return false
default:
return true
}
}
func paymentMeta(p *orchestrationv2.Payment) map[string]string {
if p == nil {
return nil
}
meta := make(map[string]string)
if quotationRef := strings.TrimSpace(p.GetQuotationRef()); quotationRef != "" {
meta["quotationRef"] = quotationRef
}
if clientPaymentRef := strings.TrimSpace(p.GetClientPaymentRef()); clientPaymentRef != "" {
meta["clientPaymentRef"] = clientPaymentRef
}
if version := p.GetVersion(); version > 0 {
meta["version"] = strconv.FormatUint(version, 10)
}
if len(meta) == 0 {
return nil
}
return meta
}
func timestampAsTime(ts *timestamppb.Timestamp) time.Time {
if ts == nil {
return time.Time{}
}
return ts.AsTime()
}
func enumJSONName(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}