payment rails

This commit is contained in:
Stephan D
2025-12-10 18:40:55 +01:00
parent 81d2db394b
commit 47899e25d4
39 changed files with 3043 additions and 909 deletions

View File

@@ -0,0 +1,211 @@
package srequest
import (
"encoding/json"
"github.com/tech/sendico/pkg/merrors"
)
type EndpointType string
const (
EndpointTypeLedger EndpointType = "ledger"
EndpointTypeManagedWallet EndpointType = "managed_wallet"
EndpointTypeExternalChain EndpointType = "external_chain"
EndpointTypeCard EndpointType = "card"
)
// Endpoint is a discriminated union for payment endpoints.
type Endpoint struct {
Type EndpointType `json:"type"`
Data json.RawMessage `json:"data"`
Metadata map[string]string `json:"metadata,omitempty"`
}
func newEndpoint(kind EndpointType, payload interface{}, metadata map[string]string) (Endpoint, error) {
data, err := json.Marshal(payload)
if err != nil {
return Endpoint{}, merrors.Internal("marshal endpoint payload failed")
}
return Endpoint{
Type: kind,
Data: data,
Metadata: cloneStringMap(metadata),
}, nil
}
func (e Endpoint) decodePayload(expected EndpointType, dst interface{}) error {
if e.Type == "" {
return merrors.InvalidArgument("endpoint type is required")
}
if e.Type != expected {
return merrors.InvalidArgument("expected endpoint type " + string(expected) + ", got " + string(e.Type))
}
if len(e.Data) == 0 {
return merrors.InvalidArgument("endpoint data is required for type " + string(expected))
}
if err := json.Unmarshal(e.Data, dst); err != nil {
return merrors.InvalidArgument("decode " + string(expected) + " endpoint: " + err.Error())
}
return nil
}
func (e *Endpoint) UnmarshalJSON(data []byte) error {
var envelope struct {
Type EndpointType `json:"type"`
Data json.RawMessage `json:"data"`
Metadata map[string]string `json:"metadata"`
}
if err := json.Unmarshal(data, &envelope); err == nil {
if envelope.Type != "" || len(envelope.Data) > 0 {
if envelope.Type == "" {
return merrors.InvalidArgument("endpoint type is required")
}
*e = Endpoint{
Type: envelope.Type,
Data: envelope.Data,
Metadata: cloneStringMap(envelope.Metadata),
}
return nil
}
}
var legacy LegacyPaymentEndpoint
if err := json.Unmarshal(data, &legacy); err != nil {
return err
}
endpoint, err := LegacyPaymentEndpointToEndpointDTO(&legacy)
if err != nil {
return err
}
if endpoint == nil {
return merrors.InvalidArgument("endpoint payload is empty")
}
*e = *endpoint
return nil
}
func NewLedgerEndpointDTO(payload LedgerEndpoint, metadata map[string]string) (Endpoint, error) {
return newEndpoint(EndpointTypeLedger, payload, metadata)
}
func NewManagedWalletEndpointDTO(payload ManagedWalletEndpoint, metadata map[string]string) (Endpoint, error) {
return newEndpoint(EndpointTypeManagedWallet, payload, metadata)
}
func NewExternalChainEndpointDTO(payload ExternalChainEndpoint, metadata map[string]string) (Endpoint, error) {
return newEndpoint(EndpointTypeExternalChain, payload, metadata)
}
func NewCardEndpointDTO(payload CardEndpoint, metadata map[string]string) (Endpoint, error) {
return newEndpoint(EndpointTypeCard, payload, metadata)
}
func (e Endpoint) DecodeLedger() (LedgerEndpoint, error) {
var payload LedgerEndpoint
return payload, e.decodePayload(EndpointTypeLedger, &payload)
}
func (e Endpoint) DecodeManagedWallet() (ManagedWalletEndpoint, error) {
var payload ManagedWalletEndpoint
return payload, e.decodePayload(EndpointTypeManagedWallet, &payload)
}
func (e Endpoint) DecodeExternalChain() (ExternalChainEndpoint, error) {
var payload ExternalChainEndpoint
return payload, e.decodePayload(EndpointTypeExternalChain, &payload)
}
func (e Endpoint) DecodeCard() (CardEndpoint, error) {
var payload CardEndpoint
return payload, e.decodePayload(EndpointTypeCard, &payload)
}
func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint, error) {
if old == nil {
return nil, nil
}
count := 0
var endpoint Endpoint
var err error
if old.Ledger != nil {
count++
endpoint, err = NewLedgerEndpointDTO(*old.Ledger, old.Metadata)
}
if old.ManagedWallet != nil {
count++
endpoint, err = NewManagedWalletEndpointDTO(*old.ManagedWallet, old.Metadata)
}
if old.ExternalChain != nil {
count++
endpoint, err = NewExternalChainEndpointDTO(*old.ExternalChain, old.Metadata)
}
if old.Card != nil {
count++
endpoint, err = NewCardEndpointDTO(*old.Card, old.Metadata)
}
if err != nil {
return nil, err
}
if count == 0 {
return nil, merrors.InvalidArgument("exactly one endpoint must be set")
}
if count > 1 {
return nil, merrors.InvalidArgument("only one endpoint can be set")
}
return &endpoint, nil
}
func EndpointDTOToLegacyPaymentEndpoint(new *Endpoint) (*LegacyPaymentEndpoint, error) {
if new == nil {
return nil, nil
}
legacy := &LegacyPaymentEndpoint{
Metadata: cloneStringMap(new.Metadata),
}
switch new.Type {
case EndpointTypeLedger:
payload, err := new.DecodeLedger()
if err != nil {
return nil, err
}
legacy.Ledger = &payload
case EndpointTypeManagedWallet:
payload, err := new.DecodeManagedWallet()
if err != nil {
return nil, err
}
legacy.ManagedWallet = &payload
case EndpointTypeExternalChain:
payload, err := new.DecodeExternalChain()
if err != nil {
return nil, err
}
legacy.ExternalChain = &payload
case EndpointTypeCard:
payload, err := new.DecodeCard()
if err != nil {
return nil, err
}
legacy.Card = &payload
default:
return nil, merrors.InvalidArgument("unsupported endpoint type: " + string(new.Type))
}
return legacy, nil
}
func cloneStringMap(src map[string]string) map[string]string {
if len(src) == 0 {
return nil
}
dst := make(map[string]string, len(src))
for k, v := range src {
dst[k] = v
}
return dst
}