Files
sendico/api/server/internal/server/paymethodsimp/service.go
2026-02-12 21:10:33 +01:00

354 lines
11 KiB
Go

package paymethodsimp
import (
"context"
"encoding/json"
"io"
"net/http"
"os"
"strings"
"time"
methodsclient "github.com/tech/sendico/payments/methods/client"
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
eapi "github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/wrapperspb"
)
type PaymentMethodsAPI struct {
logger mlogger.Logger
client methodsclient.Client
oph mutil.ParamHelper
rph mutil.ParamHelper
mph mutil.ParamHelper
}
func (a *PaymentMethodsAPI) Name() mservice.Type {
return mservice.PaymentMethods
}
func (a *PaymentMethodsAPI) Finish(_ context.Context) error {
if a.client != nil {
return a.client.Close()
}
return nil
}
func CreateAPI(apiCtx eapi.API) (*PaymentMethodsAPI, error) {
logger := apiCtx.Logger().Named(mservice.PaymentMethods)
cfg := apiCtx.Config().PaymentMethods
if cfg == nil {
return nil, merrors.InvalidArgument("payment methods configuration is not provided")
}
address, err := resolveClientAddress("payment methods", cfg)
if err != nil {
return nil, err
}
clientCfg := methodsclient.Config{
Address: address,
DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second,
CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second,
Insecure: cfg.Insecure,
}
client, err := methodsclient.New(context.Background(), clientCfg)
if err != nil {
return nil, err
}
res := &PaymentMethodsAPI{
logger: logger,
client: client,
oph: mutil.CreatePH(mservice.Organizations),
rph: mutil.CreatePH(mservice.Recipients),
mph: mutil.CreatePH(mservice.PaymentMethods),
}
apiCtx.Register().AccountHandler(res.Name(), res.oph.AddRef("/"), api.Post, res.create)
apiCtx.Register().AccountHandler(res.Name(), res.rph.AddRef(res.oph.AddRef("/list")), api.Get, res.list)
apiCtx.Register().AccountHandler(res.Name(), res.mph.AddRef("/"), api.Get, res.get)
apiCtx.Register().AccountHandler(res.Name(), "/", api.Put, res.update)
apiCtx.Register().AccountHandler(res.Name(), res.mph.AddRef("/"), api.Delete, res.delete)
apiCtx.Register().AccountHandler(res.Name(), res.mph.AddRef(res.oph.AddRef("/archive")), api.Get, res.archive)
return res, nil
}
func (a *PaymentMethodsAPI) create(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
orgRef, err := a.oph.GetRef(r)
if err != nil {
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
}
payload, err := io.ReadAll(r.Body)
if err != nil {
return response.BadPayload(a.logger, a.Name(), err)
}
resp, err := a.client.CreatePaymentMethod(r.Context(), &methodsv1.CreatePaymentMethodRequest{
AccountRef: account.ID.Hex(),
OrganizationRef: orgRef.Hex(),
PaymentMethodJson: payload,
})
if err != nil {
return grpcErrorResponse(a.logger, a.Name(), err)
}
pm, err := decodePaymentMethod(resp.GetPaymentMethodJson())
if err != nil {
return response.Internal(a.logger, a.Name(), err)
}
return sresponse.ObjectAuthCreated(a.logger, pm, token, a.Name())
}
func (a *PaymentMethodsAPI) list(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
orgRef, err := a.oph.GetRef(r)
if err != nil {
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
}
recipientRef, err := a.rph.GetRef(r)
if err != nil {
return response.BadReference(a.logger, a.Name(), a.rph.Name(), a.rph.GetID(r), err)
}
cursor, err := mutil.GetViewCursor(a.logger, r)
if err != nil {
return response.Auto(a.logger, a.Name(), err)
}
resp, err := a.client.ListPaymentMethods(r.Context(), &methodsv1.ListPaymentMethodsRequest{
AccountRef: account.ID.Hex(),
OrganizationRef: orgRef.Hex(),
RecipientRef: recipientRef.Hex(),
Cursor: toProtoCursor(cursor),
})
if err != nil {
return grpcErrorResponse(a.logger, a.Name(), err)
}
items, err := decodePaymentMethods(resp.GetPaymentMethodsJson())
if err != nil {
return response.Internal(a.logger, a.Name(), err)
}
return sresponse.ObjectsAuth(a.logger, items, token, a.Name())
}
func (a *PaymentMethodsAPI) get(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
methodRef, err := a.mph.GetRef(r)
if err != nil {
return response.BadReference(a.logger, a.Name(), a.mph.Name(), a.mph.GetID(r), err)
}
resp, err := a.client.GetPaymentMethod(r.Context(), &methodsv1.GetPaymentMethodRequest{
AccountRef: account.ID.Hex(),
PaymentMethodRef: methodRef.Hex(),
})
if err != nil {
return grpcErrorResponse(a.logger, a.Name(), err)
}
pm, err := decodePaymentMethod(resp.GetPaymentMethodJson())
if err != nil {
return response.Internal(a.logger, a.Name(), err)
}
return sresponse.ObjectAuth(a.logger, pm, token, a.Name())
}
func (a *PaymentMethodsAPI) update(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
payload, err := io.ReadAll(r.Body)
if err != nil {
return response.BadPayload(a.logger, a.Name(), err)
}
resp, err := a.client.UpdatePaymentMethod(r.Context(), &methodsv1.UpdatePaymentMethodRequest{
AccountRef: account.ID.Hex(),
PaymentMethodJson: payload,
})
if err != nil {
return grpcErrorResponse(a.logger, a.Name(), err)
}
pm, err := decodePaymentMethod(resp.GetPaymentMethodJson())
if err != nil {
return response.Internal(a.logger, a.Name(), err)
}
return sresponse.ObjectAuth(a.logger, pm, token, a.Name())
}
func (a *PaymentMethodsAPI) delete(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
methodRef, err := a.mph.GetRef(r)
if err != nil {
return response.BadReference(a.logger, a.Name(), a.mph.Name(), a.mph.GetID(r), err)
}
cascade, err := mutil.GetCascadeParam(a.logger, r)
if err != nil {
return response.Auto(a.logger, a.Name(), err)
}
cascadeValue := false
if cascade != nil {
cascadeValue = *cascade
}
_, err = a.client.DeletePaymentMethod(r.Context(), &methodsv1.DeletePaymentMethodRequest{
AccountRef: account.ID.Hex(),
PaymentMethodRef: methodRef.Hex(),
Cascade: cascadeValue,
})
if err != nil {
return grpcErrorResponse(a.logger, a.Name(), err)
}
return sresponse.ObjectsAuth(a.logger, []model.PaymentMethod{}, token, a.Name())
}
func (a *PaymentMethodsAPI) archive(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
methodRef, err := a.mph.GetRef(r)
if err != nil {
return response.BadReference(a.logger, a.Name(), a.mph.Name(), a.mph.GetID(r), err)
}
orgRef, err := a.oph.GetRef(r)
if err != nil {
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
}
archived, err := mutil.GetArchiveParam(a.logger, r)
if err != nil {
return response.Auto(a.logger, a.Name(), err)
}
if archived == nil {
return response.BadRequest(a.logger, a.Name(), "invalid_query_parameter", "'archived' param must be present")
}
cascade, err := mutil.GetCascadeParam(a.logger, r)
if err != nil {
return response.Auto(a.logger, a.Name(), err)
}
cascadeValue := false
if cascade != nil {
cascadeValue = *cascade
}
_, err = a.client.SetPaymentMethodArchived(r.Context(), &methodsv1.SetPaymentMethodArchivedRequest{
AccountRef: account.ID.Hex(),
OrganizationRef: orgRef.Hex(),
PaymentMethodRef: methodRef.Hex(),
Archived: *archived,
Cascade: cascadeValue,
})
if err != nil {
return grpcErrorResponse(a.logger, a.Name(), err)
}
return sresponse.ObjectsAuth(a.logger, []model.PaymentMethod{}, token, a.Name())
}
func resolveClientAddress(service string, cfg *eapi.PaymentOrchestratorConfig) (string, error) {
if cfg == nil {
return "", merrors.InvalidArgument(strings.TrimSpace(service) + " configuration is not provided")
}
address := strings.TrimSpace(cfg.Address)
if address != "" {
return address, nil
}
if env := strings.TrimSpace(cfg.AddressEnv); env != "" {
if resolved := strings.TrimSpace(os.Getenv(env)); resolved != "" {
return resolved, nil
}
return "", merrors.InvalidArgument(service + " address is not specified and address env " + env + " is empty")
}
return "", merrors.InvalidArgument(strings.TrimSpace(service) + " address is not specified")
}
func toProtoCursor(cursor *model.ViewCursor) *methodsv1.ViewCursor {
if cursor == nil {
return nil
}
res := &methodsv1.ViewCursor{}
hasAny := false
if cursor.Limit != nil {
res.Limit = wrapperspb.Int64(*cursor.Limit)
hasAny = true
}
if cursor.Offset != nil {
res.Offset = wrapperspb.Int64(*cursor.Offset)
hasAny = true
}
if cursor.IsArchived != nil {
res.IsArchived = wrapperspb.Bool(*cursor.IsArchived)
hasAny = true
}
if !hasAny {
return nil
}
return res
}
func decodePaymentMethod(payload []byte) (*model.PaymentMethod, error) {
var pm model.PaymentMethod
if err := json.Unmarshal(payload, &pm); err != nil {
return nil, err
}
return &pm, nil
}
func decodePaymentMethods(items [][]byte) ([]model.PaymentMethod, error) {
if len(items) == 0 {
return nil, nil
}
res := make([]model.PaymentMethod, 0, len(items))
for i := range items {
pm, err := decodePaymentMethod(items[i])
if err != nil {
return nil, err
}
res = append(res, *pm)
}
return res, nil
}
func grpcErrorResponse(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc {
statusErr, ok := status.FromError(err)
if !ok {
return response.Internal(logger, source, err)
}
switch statusErr.Code() {
case codes.InvalidArgument:
return response.BadRequest(logger, source, "invalid_argument", statusErr.Message())
case codes.NotFound:
return response.NotFound(logger, source, statusErr.Message())
case codes.PermissionDenied:
return response.AccessDenied(logger, source, statusErr.Message())
case codes.Unauthenticated:
return response.Unauthorized(logger, source, statusErr.Message())
case codes.AlreadyExists, codes.Aborted:
return response.DataConflict(logger, source, statusErr.Message())
case codes.Unimplemented:
return response.NotImplemented(logger, source, statusErr.Message())
case codes.FailedPrecondition:
return response.Error(logger, source, http.StatusPreconditionFailed, "failed_precondition", statusErr.Message())
case codes.DeadlineExceeded:
return response.Error(logger, source, http.StatusGatewayTimeout, "deadline_exceeded", statusErr.Message())
case codes.Unavailable:
return response.Error(logger, source, http.StatusServiceUnavailable, "service_unavailable", statusErr.Message())
default:
return response.Internal(logger, source, err)
}
}