package paymentapiimp import ( "net/http" "strconv" "strings" "time" "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" orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2" "github.com/tech/sendico/server/interface/api/sresponse" mutil "github.com/tech/sendico/server/internal/mutil/param" "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" "google.golang.org/protobuf/types/known/timestamppb" ) 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, bson.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 := &orchestrationv2.ListPaymentsRequest{Meta: requestMeta(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 quotationRef := firstNonEmpty(query.Get("quotation_ref"), query.Get("quote_ref")); quotationRef != "" { req.QuotationRef = quotationRef } createdFrom, err := parseRFC3339Timestamp(firstNonEmpty(query.Get("created_from"), query.Get("createdFrom")), "created_from") if err != nil { return response.Auto(a.logger, a.Name(), err) } if createdFrom != nil { req.CreatedFrom = createdFrom } createdTo, err := parseRFC3339Timestamp(firstNonEmpty(query.Get("created_to"), query.Get("createdTo")), "created_to") if err != nil { return response.Auto(a.logger, a.Name(), err) } if createdTo != nil { req.CreatedTo = createdTo } if req.GetCreatedFrom() != nil && req.GetCreatedTo() != nil { if !req.GetCreatedTo().AsTime().After(req.GetCreatedFrom().AsTime()) { return response.Auto(a.logger, a.Name(), merrors.InvalidArgument("created_to must be after created_from", "created_to")) } } if states, err := parsePaymentStateFilters(r); err != nil { return response.Auto(a.logger, a.Name(), err) } else if len(states) > 0 { req.States = states } resp, err := a.execution.ListPayments(ctx, req) if err != nil { a.logger.Warn("Failed to list payments", zap.Error(err), mzap.ObjRef("organization_ref", orgRef)) return grpcErrorResponse(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) ([]orchestrationv2.OrchestrationState, 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([]orchestrationv2.OrchestrationState, 0, len(values)) for _, raw := range values { for _, part := range strings.Split(raw, ",") { trimmed := strings.TrimSpace(part) if trimmed == "" { continue } state, ok := orchestrationStateFromString(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 orchestrationStateFromString(value string) (orchestrationv2.OrchestrationState, bool) { upper := strings.ToUpper(strings.TrimSpace(value)) if upper == "" { return 0, false } switch upper { case "PAYMENT_STATE_ACCEPTED", "ACCEPTED": return orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_CREATED, true case "PAYMENT_STATE_FUNDS_RESERVED", "FUNDS_RESERVED", "PAYMENT_STATE_SUBMITTED", "SUBMITTED": return orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_EXECUTING, true case "PAYMENT_STATE_SETTLED", "SETTLED": return orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_SETTLED, true case "PAYMENT_STATE_FAILED", "FAILED", "PAYMENT_STATE_CANCELLED", "CANCELLED": return orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_FAILED, true } if !strings.HasPrefix(upper, "ORCHESTRATION_STATE_") { upper = "ORCHESTRATION_STATE_" + upper } enumValue, ok := orchestrationv2.OrchestrationState_value[upper] if !ok { return 0, false } return orchestrationv2.OrchestrationState(enumValue), true } func firstNonEmpty(values ...string) string { for _, value := range values { trimmed := strings.TrimSpace(value) if trimmed != "" { return trimmed } } return "" } func parseRFC3339Timestamp(raw string, field string) (*timestamppb.Timestamp, error) { trimmed := strings.TrimSpace(raw) if trimmed == "" { return nil, nil } parsed, err := time.Parse(time.RFC3339, trimmed) if err != nil { return nil, merrors.InvalidArgument("invalid "+field+", expected RFC3339", field) } return timestamppb.New(parsed), nil }