200 lines
6.1 KiB
Go
200 lines
6.1 KiB
Go
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
|
|
}
|