neq quotation definition + priced_at field

This commit is contained in:
Stephan D
2026-02-13 15:35:17 +01:00
parent da1636014b
commit 52c4c046c9
85 changed files with 1180 additions and 162 deletions

View File

@@ -11,7 +11,8 @@ require (
github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1
google.golang.org/grpc v1.79.0
google.golang.org/grpc v1.79.1
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
)
@@ -48,5 +49,4 @@ require (
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -210,8 +210,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA=
google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -24,7 +24,7 @@ func (s *Service) CreatePaymentMethod(ctx context.Context, req *methodsv1.Create
return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err)
}
pm, err := decodePaymentMethod(req.GetPaymentMethodJson())
pm, err := decodePaymentMethodPayload(req.GetPaymentMethod(), "payment_method")
if err != nil {
return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err)
}
@@ -32,10 +32,10 @@ func (s *Service) CreatePaymentMethod(ctx context.Context, req *methodsv1.Create
return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err)
}
payload, err := encodePaymentMethod(pm)
record, err := encodePaymentMethodRecord(pm)
if err != nil {
return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err)
}
return &methodsv1.CreatePaymentMethodResponse{PaymentMethodJson: payload}, nil
return &methodsv1.CreatePaymentMethodResponse{PaymentMethodRecord: record}, nil
}

View File

@@ -29,10 +29,10 @@ func (s *Service) GetPaymentMethod(ctx context.Context, req *methodsv1.GetPaymen
return autoError[methodsv1.GetPaymentMethodResponse](ctx, s.logger, err)
}
payload, err := encodePaymentMethod(pm)
record, err := encodePaymentMethodRecord(pm)
if err != nil {
return autoError[methodsv1.GetPaymentMethodResponse](ctx, s.logger, err)
}
return &methodsv1.GetPaymentMethodResponse{PaymentMethodJson: payload}, nil
return &methodsv1.GetPaymentMethodResponse{PaymentMethodRecord: record}, nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/tech/sendico/pkg/merrors"
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
)
@@ -33,16 +34,16 @@ func (s *Service) ListPaymentMethods(ctx context.Context, req *methodsv1.ListPay
return autoError[methodsv1.ListPaymentMethodsResponse](ctx, s.logger, err)
}
result := make([][]byte, 0, len(items))
result := make([]*endpointv1.PaymentMethodRecord, 0, len(items))
for i := range items {
payload, err := encodePaymentMethod(&items[i])
record, err := encodePaymentMethodRecord(&items[i])
if err != nil {
return autoError[methodsv1.ListPaymentMethodsResponse](ctx, s.logger, err)
}
result = append(result, payload)
result = append(result, record)
}
return &methodsv1.ListPaymentMethodsResponse{
PaymentMethodsJson: result,
PaymentMethods: result,
}, nil
}

View File

@@ -20,7 +20,7 @@ func (s *Service) UpdatePaymentMethod(ctx context.Context, req *methodsv1.Update
return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, err)
}
pm, err := decodePaymentMethod(req.GetPaymentMethodJson())
pm, err := decodePaymentMethodRecord(req.GetPaymentMethodRecord())
if err != nil {
return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, err)
}
@@ -28,10 +28,10 @@ func (s *Service) UpdatePaymentMethod(ctx context.Context, req *methodsv1.Update
return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, err)
}
payload, err := encodePaymentMethod(pm)
record, err := encodePaymentMethodRecord(pm)
if err != nil {
return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, err)
}
return &methodsv1.UpdatePaymentMethodResponse{PaymentMethodJson: payload}, nil
return &methodsv1.UpdatePaymentMethodResponse{PaymentMethodRecord: record}, nil
}

View File

@@ -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 {