Payments listing method #209
@@ -22,7 +22,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
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/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||||
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
|
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
|
||||||
|
|||||||
@@ -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/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 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
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-20251229120209-a0d175451f7b h1:g/wCbvJGhOAqfGBjWnqtD6CVsXdr3G4GCbjLR6z9kNw=
|
||||||
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/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||||
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
|
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/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
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"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
)
|
)
|
||||||
@@ -75,7 +76,8 @@ type paymentQuotesResponse struct {
|
|||||||
|
|
||||||
type paymentsResponse struct {
|
type paymentsResponse struct {
|
||||||
authResponse `json:",inline"`
|
authResponse `json:",inline"`
|
||||||
Payments []Payment `json:"payments"`
|
Payments []Payment `json:"payments"`
|
||||||
|
Page *paginationv1.CursorPageResponse `json:"page,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type paymentResponse struct {
|
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.
|
// Payment wraps a payment with refreshed access token.
|
||||||
func PaymentResponse(logger mlogger.Logger, payment *orchestratorv1.Payment, token *TokenData) http.HandlerFunc {
|
func PaymentResponse(logger mlogger.Logger, payment *orchestratorv1.Payment, token *TokenData) http.HandlerFunc {
|
||||||
return response.Ok(logger, paymentResponse{
|
return response.Ok(logger, paymentResponse{
|
||||||
|
|||||||
153
api/server/internal/server/paymentapiimp/list.go
Normal file
153
api/server/internal/server/paymentapiimp/list.go
Normal 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
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ type paymentClient interface {
|
|||||||
QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error)
|
QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error)
|
||||||
InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
|
InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
|
||||||
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
|
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
|
||||||
|
ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error)
|
||||||
Close() 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("/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-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("/by-multiquote"), api.Post, p.initiatePaymentsByQuote)
|
||||||
|
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listPayments)
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|||||||
20
frontend/pshared/lib/api/responses/payment/payments.dart
Normal file
20
frontend/pshared/lib/api/responses/payment/payments.dart
Normal 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);
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import 'package:pshared/api/requests/payment/initiate.dart';
|
import 'package:pshared/api/requests/payment/initiate.dart';
|
||||||
import 'package:pshared/api/responses/payment/payment.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/data/mapper/payment/payment_response.dart';
|
||||||
import 'package:pshared/models/payment/payment.dart';
|
import 'package:pshared/models/payment/payment.dart';
|
||||||
import 'package:pshared/service/authorization/service.dart';
|
import 'package:pshared/service/authorization/service.dart';
|
||||||
@@ -13,6 +15,40 @@ class PaymentService {
|
|||||||
static final _logger = Logger('service.payment');
|
static final _logger = Logger('service.payment');
|
||||||
static const String _objectType = Services.payments;
|
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(
|
static Future<Payment> pay(
|
||||||
String organizationRef,
|
String organizationRef,
|
||||||
String quotationRef, {
|
String quotationRef, {
|
||||||
|
|||||||
Reference in New Issue
Block a user