package methods import ( "context" "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) { return gsresponse.Execute(ctx, gsresponse.Auto[T](logger, mservice.PaymentMethods, err)) } func parseObjectID(value, field string) (bson.ObjectID, error) { trimmed := strings.TrimSpace(value) if trimmed == "" { return bson.NilObjectID, merrors.InvalidArgument(fmt.Sprintf("%s is required", field), field) } 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 decodePaymentMethodRecord(record *endpointv1.PaymentMethodRecord) (*model.PaymentMethod, error) { if record == nil { return nil, merrors.InvalidArgument("payment_method_record is required", "payment_method_record") } 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 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") } pt, err := paymentTypeToProto(pm.Type) if err != nil { return nil, err } 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 { if cursor == nil { return nil } res := &model.ViewCursor{} hasAny := false if limit := cursor.GetLimit(); limit != nil { v := limit.GetValue() res.Limit = &v hasAny = true } if offset := cursor.GetOffset(); offset != nil { v := offset.GetValue() res.Offset = &v hasAny = true } if archived := cursor.GetIsArchived(); archived != nil { v := archived.GetValue() res.IsArchived = &v hasAny = true } if !hasAny { return nil } return res }