|
|
|
|
@@ -2,17 +2,24 @@ package methods
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
|
|
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
|
|
|
"github.com/tech/sendico/pkg/mlogger"
|
|
|
|
|
"github.com/tech/sendico/pkg/model"
|
|
|
|
|
"github.com/tech/sendico/pkg/mservice"
|
|
|
|
|
archivablev1 "github.com/tech/sendico/pkg/proto/common/archivable/v1"
|
|
|
|
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
|
|
|
|
oboundv1 "github.com/tech/sendico/pkg/proto/common/organization_bound/v1"
|
|
|
|
|
paginationv2 "github.com/tech/sendico/pkg/proto/common/pagination/v2"
|
|
|
|
|
pboundv1 "github.com/tech/sendico/pkg/proto/common/permission_bound/v1"
|
|
|
|
|
storablev1 "github.com/tech/sendico/pkg/proto/common/storable/v1"
|
|
|
|
|
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
|
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func autoError[T any](ctx context.Context, logger mlogger.Logger, err error) (*T, error) {
|
|
|
|
|
@@ -31,26 +38,228 @@ func parseObjectID(value, field string) (bson.ObjectID, error) {
|
|
|
|
|
return ref, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func decodePaymentMethod(data []byte) (*model.PaymentMethod, error) {
|
|
|
|
|
if len(data) == 0 {
|
|
|
|
|
return nil, merrors.InvalidArgument("payment_method_json is required", "payment_method_json")
|
|
|
|
|
func decodePaymentMethodRecord(record *endpointv1.PaymentMethodRecord) (*model.PaymentMethod, error) {
|
|
|
|
|
if record == nil {
|
|
|
|
|
return nil, merrors.InvalidArgument("payment_method_record is required", "payment_method_record")
|
|
|
|
|
}
|
|
|
|
|
res := &model.PaymentMethod{}
|
|
|
|
|
if err := json.Unmarshal(data, res); err != nil {
|
|
|
|
|
return nil, merrors.InvalidArgumentWrap(err, "failed to decode payment method", "payment_method_json")
|
|
|
|
|
res, err := decodePaymentMethodPayload(record.GetPaymentMethod(), "payment_method_record.payment_method")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := applyPermissionBoundRecord(res, record.GetPermissionBound()); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return res, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func encodePaymentMethod(pm *model.PaymentMethod) ([]byte, error) {
|
|
|
|
|
func decodePaymentMethodPayload(method *endpointv1.PaymentMethod, field string) (*model.PaymentMethod, error) {
|
|
|
|
|
if method == nil {
|
|
|
|
|
return nil, merrors.InvalidArgument(field+" is required", field)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
recipientRef, err := parseObjectID(method.GetRecipientRef(), field+".recipient_ref")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
pt, err := paymentTypeFromProto(method.GetType(), field+".type")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &model.PaymentMethod{
|
|
|
|
|
Describable: describableFromProto(method.GetDescribable()),
|
|
|
|
|
RecipientRef: recipientRef,
|
|
|
|
|
Type: pt,
|
|
|
|
|
Data: cloneBytes(method.GetData()),
|
|
|
|
|
IsMain: method.GetIsMain(),
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func encodePaymentMethodRecord(pm *model.PaymentMethod) (*endpointv1.PaymentMethodRecord, error) {
|
|
|
|
|
if pm == nil {
|
|
|
|
|
return nil, merrors.InvalidArgument("payment method is required")
|
|
|
|
|
}
|
|
|
|
|
payload, err := json.Marshal(pm)
|
|
|
|
|
pt, err := paymentTypeToProto(pm.Type)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, merrors.InternalWrap(err, "failed to encode payment method")
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return payload, nil
|
|
|
|
|
|
|
|
|
|
return &endpointv1.PaymentMethodRecord{
|
|
|
|
|
PermissionBound: permissionBoundFromModel(pm),
|
|
|
|
|
PaymentMethod: &endpointv1.PaymentMethod{
|
|
|
|
|
Describable: describableToProto(pm.Describable),
|
|
|
|
|
RecipientRef: toObjectHex(pm.RecipientRef),
|
|
|
|
|
Type: pt,
|
|
|
|
|
Data: cloneBytes(pm.Data),
|
|
|
|
|
IsMain: pm.IsMain,
|
|
|
|
|
},
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func paymentTypeFromProto(value endpointv1.PaymentMethodType, field string) (model.PaymentType, error) {
|
|
|
|
|
switch value {
|
|
|
|
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
|
|
|
|
|
return model.PaymentTypeIban, nil
|
|
|
|
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
|
|
|
|
|
return model.PaymentTypeCard, nil
|
|
|
|
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN:
|
|
|
|
|
return model.PaymentTypeCardToken, nil
|
|
|
|
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
|
|
|
|
|
return model.PaymentTypeBankAccount, nil
|
|
|
|
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET:
|
|
|
|
|
return model.PaymentTypeWallet, nil
|
|
|
|
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS:
|
|
|
|
|
return model.PaymentTypeCryptoAddress, nil
|
|
|
|
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER:
|
|
|
|
|
return model.PaymentTypeLedger, nil
|
|
|
|
|
default:
|
|
|
|
|
return model.PaymentTypeIban, merrors.InvalidArgument(fmt.Sprintf("%s has unsupported value: %s", field, value.String()), field)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func paymentTypeToProto(value model.PaymentType) (endpointv1.PaymentMethodType, error) {
|
|
|
|
|
switch value {
|
|
|
|
|
case model.PaymentTypeIban:
|
|
|
|
|
return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN, nil
|
|
|
|
|
case model.PaymentTypeCard:
|
|
|
|
|
return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD, nil
|
|
|
|
|
case model.PaymentTypeCardToken:
|
|
|
|
|
return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN, nil
|
|
|
|
|
case model.PaymentTypeBankAccount:
|
|
|
|
|
return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT, nil
|
|
|
|
|
case model.PaymentTypeWallet:
|
|
|
|
|
return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET, nil
|
|
|
|
|
case model.PaymentTypeCryptoAddress:
|
|
|
|
|
return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS, nil
|
|
|
|
|
case model.PaymentTypeLedger:
|
|
|
|
|
return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER, nil
|
|
|
|
|
default:
|
|
|
|
|
return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_UNSPECIFIED, merrors.InvalidArgument(fmt.Sprintf("unsupported payment method type: %s", value.String()), "type")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func describableFromProto(src *describablev1.Describable) model.Describable {
|
|
|
|
|
if src == nil {
|
|
|
|
|
return model.Describable{}
|
|
|
|
|
}
|
|
|
|
|
res := model.Describable{Name: src.GetName()}
|
|
|
|
|
if src.Description != nil {
|
|
|
|
|
v := src.GetDescription()
|
|
|
|
|
res.Description = &v
|
|
|
|
|
}
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func describableToProto(src model.Describable) *describablev1.Describable {
|
|
|
|
|
if strings.TrimSpace(src.Name) == "" && src.Description == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
res := &describablev1.Describable{
|
|
|
|
|
Name: src.Name,
|
|
|
|
|
}
|
|
|
|
|
if src.Description != nil {
|
|
|
|
|
v := *src.Description
|
|
|
|
|
res.Description = &v
|
|
|
|
|
}
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cloneBytes(src []byte) []byte {
|
|
|
|
|
if len(src) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
dst := make([]byte, len(src))
|
|
|
|
|
copy(dst, src)
|
|
|
|
|
return dst
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func applyPermissionBoundRecord(pm *model.PaymentMethod, src *pboundv1.PermissionBound) error {
|
|
|
|
|
if pm == nil || src == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if storable := src.GetStorable(); storable != nil {
|
|
|
|
|
if methodRef, err := parseOptionalObjectID(storable.GetId(), "payment_method_record.permission_bound.storable.id"); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
} else if methodRef != bson.NilObjectID {
|
|
|
|
|
pm.ID = methodRef
|
|
|
|
|
}
|
|
|
|
|
pm.CreatedAt = fromProtoTime(storable.GetCreatedAt())
|
|
|
|
|
pm.UpdatedAt = fromProtoTime(storable.GetUpdatedAt())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if archivable := src.GetArchivable(); archivable != nil {
|
|
|
|
|
pm.Archived = archivable.GetIsArchived()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if orgBound := src.GetOrganizationBound(); orgBound != nil {
|
|
|
|
|
if orgRef, err := parseOptionalObjectID(orgBound.GetOrganizationRef(), "payment_method_record.permission_bound.organization_bound.organization_ref"); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
} else if orgRef != bson.NilObjectID {
|
|
|
|
|
pm.SetOrganizationRef(orgRef)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if permissionRef, err := parseOptionalObjectID(src.GetPermissionRef(), "payment_method_record.permission_bound.permission_ref"); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
} else if permissionRef != bson.NilObjectID {
|
|
|
|
|
pm.SetPermissionRef(permissionRef)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func permissionBoundFromModel(pm *model.PaymentMethod) *pboundv1.PermissionBound {
|
|
|
|
|
if pm == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return &pboundv1.PermissionBound{
|
|
|
|
|
Storable: &storablev1.Storable{
|
|
|
|
|
Id: toObjectHex(pm.ID),
|
|
|
|
|
CreatedAt: toProtoTime(pm.CreatedAt),
|
|
|
|
|
UpdatedAt: toProtoTime(pm.UpdatedAt),
|
|
|
|
|
},
|
|
|
|
|
Archivable: &archivablev1.Archivable{
|
|
|
|
|
IsArchived: pm.Archived,
|
|
|
|
|
},
|
|
|
|
|
OrganizationBound: &oboundv1.OrganizationBound{
|
|
|
|
|
OrganizationRef: toObjectHex(pm.GetOrganizationRef()),
|
|
|
|
|
},
|
|
|
|
|
PermissionRef: toObjectHex(pm.GetPermissionRef()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseOptionalObjectID(value, field string) (bson.ObjectID, error) {
|
|
|
|
|
trimmed := strings.TrimSpace(value)
|
|
|
|
|
if trimmed == "" {
|
|
|
|
|
return bson.NilObjectID, nil
|
|
|
|
|
}
|
|
|
|
|
ref, err := bson.ObjectIDFromHex(trimmed)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return bson.NilObjectID, merrors.InvalidArgument(fmt.Sprintf("%s must be a valid object id", field), field)
|
|
|
|
|
}
|
|
|
|
|
return ref, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func toObjectHex(value bson.ObjectID) string {
|
|
|
|
|
if value == bson.NilObjectID {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return value.Hex()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func toProtoTime(value time.Time) *timestamppb.Timestamp {
|
|
|
|
|
if value.IsZero() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return timestamppb.New(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fromProtoTime(value *timestamppb.Timestamp) time.Time {
|
|
|
|
|
if value == nil {
|
|
|
|
|
return time.Time{}
|
|
|
|
|
}
|
|
|
|
|
return value.AsTime()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func toModelCursor(cursor *paginationv2.ViewCursor) *model.ViewCursor {
|
|
|
|
|
|