Merge pull request 'Payments listing method' (#209) from payments-208 into main
All checks were successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful

Reviewed-on: #209
This commit was merged in pull request #209.
This commit is contained in:
2025-12-29 13:29:46 +00:00
7 changed files with 226 additions and 4 deletions

View File

@@ -22,7 +22,7 @@ require (
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251225023818-8886bb81c549 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251229120209-a0d175451f7b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect

View File

@@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251225023818-8886bb81c549 h1:NERDcANvDCnspxdMEMLXOMnuITWIWrTQvvhEA8ewBBM=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251225023818-8886bb81c549/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251229120209-a0d175451f7b h1:g/wCbvJGhOAqfGBjWnqtD6CVsXdr3G4GCbjLR6z9kNw=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251229120209-a0d175451f7b/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=

View File

@@ -7,6 +7,7 @@ import (
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
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"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
)
@@ -75,7 +76,8 @@ type paymentQuotesResponse struct {
type paymentsResponse struct {
authResponse `json:",inline"`
Payments []Payment `json:"payments"`
Payments []Payment `json:"payments"`
Page *paginationv1.CursorPageResponse `json:"page,omitempty"`
}
type paymentResponse struct {
@@ -107,6 +109,15 @@ func PaymentsResponse(logger mlogger.Logger, payments []*orchestratorv1.Payment,
})
}
// PaymentsList wraps a list of payments with refreshed access token and pagination data.
func PaymentsListResponse(logger mlogger.Logger, resp *orchestratorv1.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 *orchestratorv1.Payment, token *TokenData) http.HandlerFunc {
return response.Ok(logger, paymentResponse{

View File

@@ -0,0 +1,153 @@
package paymentapiimp
import (
"net/http"
"strconv"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
const maxInt32 = int64(1<<31 - 1)
func (a *PaymentAPI) listPayments(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
orgRef, err := a.oph.GetRef(r)
if err != nil {
a.logger.Warn("Failed to parse organization reference for payments list", zap.Error(err), mutil.PLog(a.oph, r))
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
}
ctx := r.Context()
allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionRead)
if err != nil {
a.logger.Warn("Failed to check payments access permissions", zap.Error(err), mutil.PLog(a.oph, r))
return response.Auto(a.logger, a.Name(), err)
}
if !allowed {
a.logger.Debug("Access denied when listing payments", mutil.PLog(a.oph, r))
return response.AccessDenied(a.logger, a.Name(), "payments read permission denied")
}
req := &orchestratorv1.ListPaymentsRequest{
Meta: &orchestratorv1.RequestMeta{
OrganizationRef: orgRef.Hex(),
},
}
if page, err := listPaymentsPage(r); err != nil {
return response.Auto(a.logger, a.Name(), err)
} else if page != nil {
req.Page = page
}
query := r.URL.Query()
if sourceRef := strings.TrimSpace(query.Get("source_ref")); sourceRef != "" {
req.SourceRef = sourceRef
}
if destinationRef := strings.TrimSpace(query.Get("destination_ref")); destinationRef != "" {
req.DestinationRef = destinationRef
}
if states, err := parsePaymentStateFilters(r); err != nil {
return response.Auto(a.logger, a.Name(), err)
} else if len(states) > 0 {
req.FilterStates = states
}
resp, err := a.client.ListPayments(ctx, req)
if err != nil {
a.logger.Warn("Failed to list payments", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return response.Auto(a.logger, a.Name(), err)
}
return sresponse.PaymentsListResponse(a.logger, resp, token)
}
func listPaymentsPage(r *http.Request) (*paginationv1.CursorPageRequest, error) {
query := r.URL.Query()
cursor := strings.TrimSpace(query.Get("cursor"))
limitRaw := strings.TrimSpace(query.Get("limit"))
var limit int64
hasLimit := false
if limitRaw != "" {
parsed, err := strconv.ParseInt(limitRaw, 10, 32)
if err != nil {
return nil, merrors.InvalidArgument("invalid limit", "limit")
}
limit = parsed
hasLimit = true
}
if cursor == "" && !hasLimit {
return nil, nil
}
page := &paginationv1.CursorPageRequest{
Cursor: cursor,
}
if hasLimit {
if limit < 0 {
limit = 0
} else if limit > maxInt32 {
limit = maxInt32
}
page.Limit = int32(limit)
}
return page, nil
}
func parsePaymentStateFilters(r *http.Request) ([]orchestratorv1.PaymentState, error) {
query := r.URL.Query()
values := append([]string{}, query["state"]...)
values = append(values, query["states"]...)
values = append(values, query["filter_states"]...)
if len(values) == 0 {
return nil, nil
}
states := make([]orchestratorv1.PaymentState, 0, len(values))
for _, raw := range values {
for _, part := range strings.Split(raw, ",") {
trimmed := strings.TrimSpace(part)
if trimmed == "" {
continue
}
state, ok := paymentStateFromString(trimmed)
if !ok {
return nil, merrors.InvalidArgument("unsupported payment state: "+trimmed, "state")
}
states = append(states, state)
}
}
if len(states) == 0 {
return nil, nil
}
return states, nil
}
func paymentStateFromString(value string) (orchestratorv1.PaymentState, bool) {
upper := strings.ToUpper(strings.TrimSpace(value))
if upper == "" {
return 0, false
}
if !strings.HasPrefix(upper, "PAYMENT_STATE_") {
upper = "PAYMENT_STATE_" + upper
}
enumValue, ok := orchestratorv1.PaymentState_value[upper]
if !ok {
return 0, false
}
return orchestratorv1.PaymentState(enumValue), true
}

View File

@@ -25,6 +25,7 @@ type paymentClient interface {
QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error)
InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error)
Close() error
}
@@ -72,6 +73,7 @@ func CreateAPI(apiCtx eapi.API) (*PaymentAPI, error) {
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/immediate"), api.Post, p.initiateImmediate)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-quote"), api.Post, p.initiateByQuote)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-multiquote"), api.Post, p.initiatePaymentsByQuote)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listPayments)
return p, nil
}

View File

@@ -0,0 +1,20 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/api/responses/base.dart';
import 'package:pshared/api/responses/token.dart';
import 'package:pshared/data/dto/payment/payment.dart';
part 'payments.g.dart';
@JsonSerializable(explicitToJson: true)
class PaymentsResponse extends BaseAuthorizedResponse {
final List<PaymentDTO> payments;
const PaymentsResponse({required super.accessToken, required this.payments});
factory PaymentsResponse.fromJson(Map<String, dynamic> json) => _$PaymentsResponseFromJson(json);
@override
Map<String, dynamic> toJson() => _$PaymentsResponseToJson(this);
}

View File

@@ -1,8 +1,10 @@
import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart';
import 'package:pshared/api/requests/payment/initiate.dart';
import 'package:pshared/api/responses/payment/payment.dart';
import 'package:pshared/api/responses/payment/payments.dart';
import 'package:pshared/data/mapper/payment/payment_response.dart';
import 'package:pshared/models/payment/payment.dart';
import 'package:pshared/service/authorization/service.dart';
@@ -13,6 +15,40 @@ class PaymentService {
static final _logger = Logger('service.payment');
static const String _objectType = Services.payments;
static Future<List<Payment>> list(
String organizationRef, {
int? limit,
String? cursor,
String? sourceRef,
String? destinationRef,
List<String>? states,
}) async {
_logger.fine('Listing payments for organization $organizationRef');
final queryParams = <String, String>{};
if (limit != null) {
queryParams['limit'] = limit.toString();
}
if (cursor != null && cursor.isNotEmpty) {
queryParams['cursor'] = cursor;
}
if (sourceRef != null && sourceRef.isNotEmpty) {
queryParams['source_ref'] = sourceRef;
}
if (destinationRef != null && destinationRef.isNotEmpty) {
queryParams['destination_ref'] = destinationRef;
}
if (states != null && states.isNotEmpty) {
queryParams['state'] = states.join(',');
}
final path = '/$organizationRef';
final url = queryParams.isEmpty
? path
: Uri(path: path, queryParameters: queryParams).toString();
final response = await AuthorizationService.getGETResponse(_objectType, url);
return PaymentsResponse.fromJson(response).payments.map((payment) => payment.toDomain()).toList();
}
static Future<Payment> pay(
String organizationRef,
String quotationRef, {