1 Commits

Author SHA1 Message Date
Arseni
0091191d97 refactor of money utils with new money2 package 2026-03-13 03:17:29 +03:00
125 changed files with 653 additions and 1664 deletions

View File

@@ -8,7 +8,7 @@ require (
github.com/aws/aws-sdk-go-v2 v1.41.3
github.com/aws/aws-sdk-go-v2/config v1.32.11
github.com/aws/aws-sdk-go-v2/credentials v1.19.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4
github.com/jung-kurt/gofpdf v1.16.2
github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0
@@ -61,7 +61,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -30,8 +30,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7su
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0 h1:zyKY4OxzUImu+DigelJI9o49QQv8CjREs5E1CywjtIA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
@@ -229,8 +229,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -3,8 +3,7 @@ package content
// Issuer details are intentionally centralized to avoid document text drift.
const (
IssuerLegalName = "SMX Operations Limited"
IssuerLegalAddress = "Room 607, 12/F., Block C, Hong Kong Industrial Centre, 489-491 Castle Peak Road, Lai Chi Kok, HongKong"
IssuerEmail = "contact@sendico.io"
IssuerLegalAddress = "Room 607, 12/F., Block C, Hong Kong Industrial Centre, 489-491 Castle Peak Road, Lai Chi Kok, Hong Kong"
)
const (
@@ -77,74 +76,40 @@ var AcceptanceTemplate = AcceptanceTemplateContent{
type OperationDocumentContent struct {
Title string
Subtitle string
MetaCertificateNumberLabel string
MetaDateLabel string
SectionParties string
PartiesIntro string
RowServiceProvider string
RowServiceProviderAddress string
RowServiceProviderEmail string
RowClient string
RowClientAddress string
RowClientReference string
SectionSubject string
SubjectIntro string
SectionServicePeriod string
RowPeriodFrom string
RowPeriodTo string
SectionTotalAmount string
RowTotalAmount string
SectionClientConfirmation string
ConfirmationLine1 string
ConfirmationLine2 string
ConfirmationLine3 string
SectionSignatures string
SignatureServiceProviderLine string
SignatureClientNamePrefix string
SignatureClientTitleLine string
SignatureClientLine string
SectionOperationStatus string
RowOperationStatus string
RowOperationCode string
RowOperationLabel string
MetaDocumentType string
SectionOperation string
SectionFailure string
RowOrganization string
RowGatewayService string
RowOperationRef string
RowPaymentRef string
RowCode string
RowState string
RowLabel string
RowStartedAtUTC string
RowCompletedAtUTC string
RowAmount string
RowFailureCode string
RowFailureReason string
MissingValuePlaceholder string
}
var OperationDocument = OperationDocumentContent{
Title: "CERTIFICATE OF SERVICES RENDERED",
Subtitle: "Payment operation completion and acceptance statement",
MetaCertificateNumberLabel: "Certificate No.",
MetaDateLabel: "Date",
SectionParties: "PARTIES",
PartiesIntro: "This Certificate is made between:",
RowServiceProvider: "Service Provider",
RowServiceProviderAddress: "Service Provider Address",
RowServiceProviderEmail: "Service Provider Email",
RowClient: "Client",
RowClientAddress: "Client Address",
RowClientReference: "Client Reference",
SectionSubject: "SUBJECT OF THE CERTIFICATE",
SubjectIntro: "The Service Provider confirms that the following services have been fully rendered to the Client:",
SectionServicePeriod: "SERVICE PERIOD",
RowPeriodFrom: "From",
RowPeriodTo: "To",
SectionTotalAmount: "TOTAL AMOUNT",
RowTotalAmount: "Amount",
SectionClientConfirmation: "CLIENT CONFIRMATION",
ConfirmationLine1: "- the services were rendered in full;",
ConfirmationLine2: "- the services were rendered properly and within the agreed scope;",
ConfirmationLine3: "- the Client has no claims regarding the quality, quantity, or timing of the services rendered.",
SectionSignatures: "SIGNATURES",
SignatureServiceProviderLine: "Service Provider: Name: SMX Operations Limited | Signature: __________________",
SignatureClientNamePrefix: "Client: Name:",
SignatureClientTitleLine: "Title: Authorized Representative",
SignatureClientLine: "Signature: __________________",
SectionOperationStatus: "OPERATION STATUS",
RowOperationStatus: "Operation Status",
RowOperationCode: "Operation Code",
RowOperationLabel: "Operation Label",
Title: "OPERATION BILLING DOCUMENT",
Subtitle: "Gateway operation statement",
MetaDocumentType: "Document Type: Operation",
SectionOperation: "OPERATION DETAILS",
SectionFailure: "FAILURE DETAILS",
RowOrganization: "Organization",
RowGatewayService: "Gateway Service",
RowOperationRef: "Operation Ref",
RowPaymentRef: "Payment Ref",
RowCode: "Code",
RowState: "State",
RowLabel: "Label",
RowStartedAtUTC: "Started At (UTC)",
RowCompletedAtUTC: "Completed At (UTC)",
RowAmount: "Amount",
RowFailureCode: "Failure Code",
RowFailureReason: "Failure Reason",
MissingValuePlaceholder: "n/a",

View File

@@ -289,8 +289,6 @@ type operationSnapshot struct {
GatewayService string
OperationRef string
PaymentRef string
ClientName string
ClientAddress string
OperationCode string
OperationLabel string
OperationState string
@@ -308,8 +306,6 @@ func operationSnapshotFromRequest(req *documentsv1.GetOperationDocumentRequest)
GatewayService: strings.TrimSpace(req.GetGatewayService()),
OperationRef: strings.TrimSpace(req.GetOperationRef()),
PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
ClientName: strings.TrimSpace(req.GetClientName()),
ClientAddress: strings.TrimSpace(req.GetClientAddress()),
OperationCode: strings.TrimSpace(req.GetOperationCode()),
OperationLabel: strings.TrimSpace(req.GetOperationLabel()),
OperationState: strings.TrimSpace(req.GetOperationState()),
@@ -332,6 +328,21 @@ func operationSnapshotFromRequest(req *documentsv1.GetOperationDocumentRequest)
func buildOperationBlocks(snapshot operationSnapshot) []renderer.Block {
documentCopy := content.OperationDocument
rows := [][]string{
{documentCopy.RowOrganization, snapshot.OrganizationRef},
{documentCopy.RowGatewayService, snapshot.GatewayService},
{documentCopy.RowOperationRef, snapshot.OperationRef},
{documentCopy.RowPaymentRef, safeValue(snapshot.PaymentRef)},
{documentCopy.RowCode, safeValue(snapshot.OperationCode)},
{documentCopy.RowState, safeValue(snapshot.OperationState)},
{documentCopy.RowLabel, safeValue(snapshot.OperationLabel)},
{documentCopy.RowStartedAtUTC, formatSnapshotTime(snapshot.StartedAt)},
{documentCopy.RowCompletedAtUTC, formatSnapshotTime(snapshot.CompletedAt)},
}
if snapshot.Amount != "" || snapshot.Currency != "" {
rows = append(rows, []string{documentCopy.RowAmount, strings.TrimSpace(strings.TrimSpace(snapshot.Amount) + " " + strings.TrimSpace(snapshot.Currency))})
}
blocks := []renderer.Block{
{
Tag: renderer.TagTitle,
@@ -344,115 +355,30 @@ func buildOperationBlocks(snapshot operationSnapshot) []renderer.Block {
{
Tag: renderer.TagMeta,
Lines: []string{
fmt.Sprintf("%s: %s", documentCopy.MetaCertificateNumberLabel, certificateNumber(snapshot)),
fmt.Sprintf("%s: %s", documentCopy.MetaDateLabel, formatCertificateDate(certificateDate(snapshot))),
documentCopy.MetaDocumentType,
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionParties},
},
{
Tag: renderer.TagText,
Lines: []string{documentCopy.PartiesIntro},
Lines: []string{documentCopy.SectionOperation},
},
{
Tag: renderer.TagKV,
Rows: [][]string{
{documentCopy.RowServiceProvider, content.IssuerLegalName},
{documentCopy.RowServiceProviderAddress, content.IssuerLegalAddress},
{documentCopy.RowServiceProviderEmail, content.IssuerEmail},
{documentCopy.RowClient, certificateClientName(snapshot)},
{documentCopy.RowClientAddress, certificateClientAddress(snapshot)},
{documentCopy.RowClientReference, safeValue(snapshot.PaymentRef)},
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionSubject},
},
{
Tag: renderer.TagText,
Lines: []string{
documentCopy.SubjectIntro,
"",
"- Payment execution and orchestration services for payment reference " + safeValue(snapshot.PaymentRef) + ".",
"- Gateway service: " + safeValue(snapshot.GatewayService) + ".",
"- Operation reference: " + safeValue(snapshot.OperationRef) + ".",
"- Operation descriptor: " + operationDescriptor(snapshot) + ".",
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionServicePeriod},
},
{
Tag: renderer.TagKV,
Rows: [][]string{
{documentCopy.RowPeriodFrom, formatSnapshotTime(snapshot.StartedAt)},
{documentCopy.RowPeriodTo, formatSnapshotTime(snapshot.CompletedAt)},
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionTotalAmount},
},
{
Tag: renderer.TagKV,
Rows: [][]string{
{documentCopy.RowTotalAmount, operationAmount(snapshot)},
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionClientConfirmation},
},
{
Tag: renderer.TagText,
Lines: []string{
documentCopy.ConfirmationLine1,
documentCopy.ConfirmationLine2,
documentCopy.ConfirmationLine3,
"",
"This Certificate serves as confirmation of the completion and acceptance of the services.",
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionSignatures},
},
{
Tag: renderer.TagSign,
Lines: []string{
documentCopy.SignatureServiceProviderLine,
"",
documentCopy.SignatureClientNamePrefix + " " + certificateClientName(snapshot),
documentCopy.SignatureClientTitleLine,
documentCopy.SignatureClientLine,
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionOperationStatus},
},
{
Tag: renderer.TagKV,
Rows: [][]string{
{documentCopy.RowOperationStatus, safeValue(snapshot.OperationState)},
{documentCopy.RowOperationCode, safeValue(snapshot.OperationCode)},
{documentCopy.RowOperationLabel, safeValue(snapshot.OperationLabel)},
},
Rows: rows,
},
}
if snapshot.FailureCode != "" || snapshot.FailureReason != "" {
blocks = append(blocks, renderer.Block{
blocks = append(blocks,
renderer.Block{Tag: renderer.TagSection, Lines: []string{documentCopy.SectionFailure}},
renderer.Block{
Tag: renderer.TagKV,
Rows: [][]string{
{documentCopy.RowFailureCode, safeValue(snapshot.FailureCode)},
{documentCopy.RowFailureReason, safeValue(snapshot.FailureReason)},
},
})
},
)
}
return blocks
@@ -466,80 +392,6 @@ func formatSnapshotTime(value time.Time) string {
return value.UTC().Format(time.RFC3339)
}
func certificateNumber(snapshot operationSnapshot) string {
if paymentRef := strings.TrimSpace(snapshot.PaymentRef); paymentRef != "" {
return paymentRef
}
if operationRef := strings.TrimSpace(snapshot.OperationRef); operationRef != "" {
return operationRef
}
return content.OperationDocument.MissingValuePlaceholder
}
func certificateDate(snapshot operationSnapshot) time.Time {
if !snapshot.CompletedAt.IsZero() {
return snapshot.CompletedAt.UTC()
}
if !snapshot.StartedAt.IsZero() {
return snapshot.StartedAt.UTC()
}
return time.Now().UTC()
}
func formatCertificateDate(value time.Time) string {
if value.IsZero() {
return content.OperationDocument.MissingValuePlaceholder
}
return value.UTC().Format("January 2, 2006")
}
func operationAmount(snapshot operationSnapshot) string {
amount := strings.TrimSpace(snapshot.Amount)
currency := strings.TrimSpace(snapshot.Currency)
if amount == "" && currency == "" {
return content.OperationDocument.MissingValuePlaceholder
}
return strings.TrimSpace(amount + " " + currency)
}
func operationDescriptor(snapshot operationSnapshot) string {
label := strings.TrimSpace(snapshot.OperationLabel)
code := strings.TrimSpace(snapshot.OperationCode)
switch {
case label != "" && code != "":
return fmt.Sprintf("%s (%s)", label, code)
case label != "":
return label
case code != "":
return code
default:
return content.OperationDocument.MissingValuePlaceholder
}
}
func certificateClientName(snapshot operationSnapshot) string {
if name := strings.TrimSpace(snapshot.ClientName); name != "" {
return name
}
return "John Doe"
}
func certificateClientAddress(snapshot operationSnapshot) string {
if address := strings.TrimSpace(snapshot.ClientAddress); address != "" {
return address
}
return content.OperationDocument.MissingValuePlaceholder
}
func safeValue(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {

View File

@@ -3,7 +3,6 @@ package documents
import (
"bytes"
"context"
"strings"
"testing"
"time"
@@ -192,92 +191,3 @@ func TestGetOperationDocument_RequiresOperationRef(t *testing.T) {
t.Fatalf("expected InvalidArgument, got=%v err=%v", status.Code(err), err)
}
}
func TestBuildOperationBlocks_CertificateIncludesPaymentData(t *testing.T) {
snapshot := operationSnapshot{
OrganizationRef: "org-1",
GatewayService: "chain_gateway",
OperationRef: "op-123",
PaymentRef: "pay-123",
ClientName: "Jane Customer",
ClientAddress: "Main Street 1, City",
OperationCode: "transfer",
OperationLabel: "Outbound transfer",
OperationState: "completed",
Amount: "100.50",
Currency: "USDT",
StartedAt: time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC),
CompletedAt: time.Date(2026, 3, 2, 12, 0, 0, 0, time.UTC),
}
blocks := buildOperationBlocks(snapshot)
if len(blocks) == 0 {
t.Fatalf("expected blocks")
}
if got := blocks[0].Lines[0]; got != content.OperationDocument.Title {
t.Fatalf("title mismatch: got=%q want=%q", got, content.OperationDocument.Title)
}
meta := findTaggedBlock(blocks, renderer.TagMeta)
if meta == nil {
t.Fatalf("expected meta block")
}
metaText := strings.Join(meta.Lines, "\n")
if !strings.Contains(metaText, "Certificate No.: pay-123") {
t.Fatalf("meta should include certificate number, got=%q", metaText)
}
if !strings.Contains(metaText, "Date: March 2, 2026") {
t.Fatalf("meta should include certificate date, got=%q", metaText)
}
amountFound := false
clientFound := false
for _, block := range blocks {
if block.Tag != renderer.TagKV {
continue
}
for _, row := range block.Rows {
if len(row) >= 2 && row[0] == content.OperationDocument.RowTotalAmount && row[1] == "100.50 USDT" {
amountFound = true
}
if len(row) >= 2 && row[0] == content.OperationDocument.RowClient && row[1] == "Jane Customer" {
clientFound = true
}
}
}
if !amountFound {
t.Fatalf("expected total amount row with payment amount")
}
if !clientFound {
t.Fatalf("expected client row with customer name")
}
}
func TestCertificateNumber_FallsBackToOperationRef(t *testing.T) {
got := certificateNumber(operationSnapshot{
OperationRef: "op-777",
})
if got != "op-777" {
t.Fatalf("certificateNumber fallback mismatch: got=%q want=%q", got, "op-777")
}
}
func TestCertificateClientName_Fallback(t *testing.T) {
if got := certificateClientName(operationSnapshot{}); got != "John Doe" {
t.Fatalf("certificateClientName fallback mismatch: got=%q want=%q", got, "John Doe")
}
}
func findTaggedBlock(blocks []renderer.Block, tag renderer.Tag) *renderer.Block {
for i := range blocks {
if blocks[i].Tag == tag {
return &blocks[i]
}
}
return nil
}

View File

@@ -46,7 +46,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -179,8 +179,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -39,7 +39,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -179,8 +179,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -18,7 +18,7 @@ require (
github.com/aws/aws-sdk-go-v2 v1.41.3
github.com/aws/aws-sdk-go-v2/config v1.32.11
github.com/aws/aws-sdk-go-v2/credentials v1.19.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/cors v1.2.2
github.com/go-chi/jwtauth/v5 v5.4.0
@@ -37,7 +37,7 @@ require (
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0
go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1
golang.org/x/net v0.52.0
golang.org/x/net v0.51.0
google.golang.org/grpc v1.79.2
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
@@ -88,7 +88,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect

View File

@@ -32,8 +32,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7su
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0 h1:zyKY4OxzUImu+DigelJI9o49QQv8CjREs5E1CywjtIA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
@@ -112,8 +112,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
@@ -346,8 +346,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@@ -16,8 +16,6 @@ import (
"github.com/tech/sendico/pkg/mutil/mzap"
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/v2/bson"
@@ -26,6 +24,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
)
const (
@@ -62,10 +61,6 @@ func (a *PaymentAPI) getOperationDocument(r *http.Request, account *model.Accoun
if operationRef == "" {
return response.BadRequest(a.logger, a.Name(), "missing_parameter", "operation_ref is required")
}
paymentRef := strings.TrimSpace(query.Get("payment_ref"))
if paymentRef == "" {
return response.BadRequest(a.logger, a.Name(), "missing_parameter", "payment_ref is required")
}
service, gateway, h := a.resolveOperationDocumentDeps(r.Context(), gatewayService)
if h != nil {
@@ -78,18 +73,7 @@ func (a *PaymentAPI) getOperationDocument(r *http.Request, account *model.Accoun
return documentErrorResponse(a.logger, a.Name(), err)
}
req := operationDocumentRequest(orgRef.Hex(), gatewayService, operationRef, paymentRef, op)
if payment, paymentErr := a.fetchPayment(r.Context(), orgRef.Hex(), paymentRef); paymentErr != nil {
a.logger.Warn(
"Failed to fetch payment snapshot for operation document",
zap.Error(paymentErr),
mzap.ObjRef("organization_ref", orgRef),
zap.String("payment_ref", paymentRef),
zap.String("operation_ref", operationRef),
)
} else {
enrichOperationDocumentRequestFromPayment(req, payment)
}
req := operationDocumentRequest(orgRef.Hex(), gatewayService, operationRef, op)
docResp, err := a.fetchOperationDocument(r.Context(), service.InvokeURI, req)
if err != nil {
@@ -279,28 +263,6 @@ func (a *PaymentAPI) fetchGatewayOperation(ctx context.Context, invokeURI, opera
return op, nil
}
func (a *PaymentAPI) fetchPayment(ctx context.Context, organizationRef, paymentRef string) (*orchestrationv2.Payment, error) {
if a.execution == nil {
return nil, merrors.Internal("payment execution client is not configured")
}
resp, err := a.execution.GetPayment(ctx, &orchestrationv2.GetPaymentRequest{
Meta: requestMeta(organizationRef, ""),
PaymentRef: strings.TrimSpace(paymentRef),
})
if err != nil {
return nil, err
}
if resp == nil {
return nil, merrors.NoData("payment orchestrator returned empty response")
}
if resp.GetPayment() == nil {
return nil, merrors.NoData("payment orchestrator returned empty payment")
}
return resp.GetPayment(), nil
}
func findGatewayForService(gateways []discovery.GatewaySummary, gatewayService mservice.Type) *discovery.GatewaySummary {
candidates := make([]discovery.GatewaySummary, 0, len(gateways))
for _, gw := range gateways {
@@ -351,14 +313,13 @@ func findGatewayForService(gateways []discovery.GatewaySummary, gatewayService m
return &best
}
func operationDocumentRequest(organizationRef string, gatewayService mservice.Type, requestedOperationRef string, paymentRef string, op *connectorv1.Operation) *documentsv1.GetOperationDocumentRequest {
func operationDocumentRequest(organizationRef string, gatewayService mservice.Type, requestedOperationRef string, op *connectorv1.Operation) *documentsv1.GetOperationDocumentRequest {
req := &documentsv1.GetOperationDocumentRequest{
OrganizationRef: strings.TrimSpace(organizationRef),
GatewayService: gatewayService,
OperationRef: firstNonEmpty(strings.TrimSpace(op.GetOperationRef()), strings.TrimSpace(requestedOperationRef)),
PaymentRef: strings.TrimSpace(paymentRef),
OperationCode: strings.TrimSpace(op.GetType().String()),
OperationLabel: strings.TrimSpace(op.GetType().String()),
OperationLabel: operationLabel(op.GetType()),
OperationState: strings.TrimSpace(op.GetStatus().String()),
Amount: strings.TrimSpace(op.GetMoney().GetAmount()),
Currency: strings.TrimSpace(op.GetMoney().GetCurrency()),
@@ -371,96 +332,65 @@ func operationDocumentRequest(organizationRef string, gatewayService mservice.Ty
req.CompletedAtUnixMs = ts.AsTime().UnixMilli()
}
if isFailedOperationStatus(op.GetStatus()) {
req.FailureCode = strings.TrimSpace(op.GetStatus().String())
}
req.PaymentRef = operationParamValue(op.GetParams(), "payment_ref", "parent_payment_ref", "paymentRef", "parentPaymentRef")
req.FailureCode = firstNonEmpty(
operationParamValue(op.GetParams(), "failure_code", "provider_code", "error_code"),
failureCodeFromStatus(op.GetStatus()),
)
req.FailureReason = operationParamValue(op.GetParams(), "failure_reason", "provider_message", "error", "message")
return req
}
func isFailedOperationStatus(status connectorv1.OperationStatus) bool {
return status == connectorv1.OperationStatus_OPERATION_FAILED || status == connectorv1.OperationStatus_OPERATION_CANCELLED
func operationLabel(opType connectorv1.OperationType) string {
switch opType {
case connectorv1.OperationType_CREDIT:
return "Credit"
case connectorv1.OperationType_DEBIT:
return "Debit"
case connectorv1.OperationType_TRANSFER:
return "Transfer"
case connectorv1.OperationType_PAYOUT:
return "Payout"
case connectorv1.OperationType_FEE_ESTIMATE:
return "Fee Estimate"
case connectorv1.OperationType_FX:
return "FX"
case connectorv1.OperationType_GAS_TOPUP:
return "Gas Top Up"
default:
return strings.TrimSpace(opType.String())
}
}
func enrichOperationDocumentRequestFromPayment(req *documentsv1.GetOperationDocumentRequest, payment *orchestrationv2.Payment) {
if req == nil || payment == nil {
return
}
req.PaymentRef = firstNonEmpty(strings.TrimSpace(req.GetPaymentRef()), strings.TrimSpace(payment.GetPaymentRef()))
req.ClientName = firstNonEmpty(strings.TrimSpace(req.GetClientName()), paymentClientName(payment))
}
func paymentClientName(payment *orchestrationv2.Payment) string {
if payment == nil {
return ""
}
intent := payment.GetIntentSnapshot()
if intent == nil {
return ""
}
if customerDescription := strings.TrimSpace(intent.GetComment()); customerDescription != "" {
return customerDescription
}
return firstNonEmpty(
paymentEndpointClientName(intent.GetDestination()),
paymentEndpointClientName(intent.GetSource()),
)
}
func paymentEndpointClientName(endpoint *endpointv1.PaymentEndpoint) string {
if endpoint == nil {
return ""
}
method := endpoint.GetPaymentMethod()
if method == nil {
return ""
}
switch method.GetType() {
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
type cardMethodData struct {
FirstName string `bson:"firstName"`
LastName string `bson:"lastName"`
}
var payload cardMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return ""
}
return strings.TrimSpace(strings.Join([]string{
strings.TrimSpace(payload.FirstName),
strings.TrimSpace(payload.LastName),
}, " "))
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
type bankMethodData struct {
RecipientName string `bson:"recipientName"`
}
var payload bankMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return ""
}
return strings.TrimSpace(payload.RecipientName)
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
type ibanMethodData struct {
AccountHolder string `bson:"accountHolder"`
}
var payload ibanMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return ""
}
return strings.TrimSpace(payload.AccountHolder)
func failureCodeFromStatus(status connectorv1.OperationStatus) string {
switch status {
case connectorv1.OperationStatus_OPERATION_FAILED, connectorv1.OperationStatus_OPERATION_CANCELLED:
return strings.TrimSpace(status.String())
default:
return ""
}
}
func operationParamValue(params *structpb.Struct, keys ...string) string {
if params == nil {
return ""
}
values := params.AsMap()
for _, key := range keys {
raw, ok := values[key]
if !ok {
continue
}
if text := strings.TrimSpace(fmt.Sprint(raw)); text != "" && text != "<nil>" {
return text
}
}
return ""
}
func findDocumentsService(services []discovery.ServiceSummary) *discovery.ServiceSummary {
for _, svc := range services {
if !strings.EqualFold(svc.Service, documentsServiceName) {

View File

@@ -1,120 +0,0 @@
package paymentapiimp
import (
"testing"
"github.com/tech/sendico/pkg/mservice"
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
"go.mongodb.org/mongo-driver/v2/bson"
)
func TestOperationDocumentRequest_UsesStructuredOperationFieldsOnly(t *testing.T) {
op := &connectorv1.Operation{
OperationRef: "pay-123:hop_1",
Type: connectorv1.OperationType_TRANSFER,
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
Money: &moneyv1.Money{
Amount: "100.50",
Currency: "USDT",
},
}
req := operationDocumentRequest("org-1", mservice.ChainGateway, "requested-op", "pay-123", op)
if req == nil {
t.Fatalf("expected request")
}
if got, want := req.GetPaymentRef(), "pay-123"; got != want {
t.Fatalf("payment_ref mismatch: got=%q want=%q", got, want)
}
if got := req.GetClientName(); got != "" {
t.Fatalf("expected empty client_name from operation-only request, got=%q", got)
}
if got := req.GetClientAddress(); got != "" {
t.Fatalf("expected empty client_address from operation-only request, got=%q", got)
}
}
func TestOperationDocumentRequest_FailureCodeFromStructuredStatus(t *testing.T) {
req := operationDocumentRequest("org-1", mservice.ChainGateway, "op", "pay-123", &connectorv1.Operation{
OperationRef: "pay-123:hop_1",
Type: connectorv1.OperationType_TRANSFER,
Status: connectorv1.OperationStatus_OPERATION_FAILED,
})
if req == nil {
t.Fatalf("expected request")
}
if got, want := req.GetFailureCode(), "OPERATION_FAILED"; got != want {
t.Fatalf("failure_code mismatch: got=%q want=%q", got, want)
}
}
func TestPaymentClientName_FromIntentComment(t *testing.T) {
payment := &orchestrationv2.Payment{
IntentSnapshot: &quotationv2.QuoteIntent{
Comment: "Jane Customer",
},
}
if got, want := paymentClientName(payment), "Jane Customer"; got != want {
t.Fatalf("paymentClientName mismatch: got=%q want=%q", got, want)
}
}
func TestPaymentClientName_FromStructuredCardEndpoint(t *testing.T) {
raw, err := bson.Marshal(map[string]string{
"firstName": "Jane",
"lastName": "Doe",
})
if err != nil {
t.Fatalf("Marshal: %v", err)
}
payment := &orchestrationv2.Payment{
IntentSnapshot: &quotationv2.QuoteIntent{
Destination: &endpointv1.PaymentEndpoint{
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
PaymentMethod: &endpointv1.PaymentMethod{
Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD,
Data: raw,
},
},
},
},
}
if got, want := paymentClientName(payment), "Jane Doe"; got != want {
t.Fatalf("paymentClientName mismatch: got=%q want=%q", got, want)
}
}
func TestEnrichOperationDocumentRequestFromPayment_SetsClientName(t *testing.T) {
req := &documentsv1.GetOperationDocumentRequest{
OperationRef: "pay-123:hop_1",
}
payment := &orchestrationv2.Payment{
PaymentRef: "pay-123",
IntentSnapshot: &quotationv2.QuoteIntent{
Comment: "Client Name",
},
}
enrichOperationDocumentRequestFromPayment(req, payment)
if got, want := req.GetPaymentRef(), "pay-123"; got != want {
t.Fatalf("payment_ref mismatch: got=%q want=%q", got, want)
}
if got, want := req.GetClientName(), "Client Name"; got != want {
t.Fatalf("client_name mismatch: got=%q want=%q", got, want)
}
}
func TestOperationDocumentRequest_Compatibility(t *testing.T) {
var _ *documentsv1.GetOperationDocumentRequest = operationDocumentRequest("org-1", mservice.ChainGateway, "op", "pay-1", &connectorv1.Operation{})
}

View File

@@ -165,10 +165,6 @@ func (*fakeExecutionClientForBatch) ListPayments(context.Context, *orchestration
return &orchestrationv2.ListPaymentsResponse{}, nil
}
func (*fakeExecutionClientForBatch) GetPayment(context.Context, *orchestrationv2.GetPaymentRequest) (*orchestrationv2.GetPaymentResponse, error) {
return &orchestrationv2.GetPaymentResponse{}, nil
}
func (*fakeExecutionClientForBatch) Close() error { return nil }
type fakeEnforcerForBatch struct {

View File

@@ -32,7 +32,6 @@ import (
type executionClient interface {
ExecutePayment(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error)
ExecuteBatchPayment(ctx context.Context, req *orchestrationv2.ExecuteBatchPaymentRequest) (*orchestrationv2.ExecuteBatchPaymentResponse, error)
GetPayment(ctx context.Context, req *orchestrationv2.GetPaymentRequest) (*orchestrationv2.GetPaymentResponse, error)
ListPayments(ctx context.Context, req *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error)
Close() error
}

View File

@@ -53,7 +53,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -212,8 +212,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -13,7 +13,7 @@ require (
github.com/tech/sendico/fx/storage v0.0.0
github.com/tech/sendico/pkg v0.1.0
go.uber.org/zap v1.27.1
golang.org/x/net v0.52.0
golang.org/x/net v0.51.0
gopkg.in/yaml.v3 v3.0.1
)

View File

@@ -179,8 +179,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -44,7 +44,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -179,8 +179,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -47,7 +47,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -181,8 +181,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -86,7 +86,7 @@ require (
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -322,8 +322,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -44,7 +44,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -179,8 +179,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -47,7 +47,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -181,8 +181,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -44,7 +44,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -179,8 +179,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -94,7 +94,7 @@ require (
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -337,8 +337,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -45,7 +45,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -181,8 +181,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -47,7 +47,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect

View File

@@ -196,8 +196,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -45,7 +45,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -181,8 +181,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -60,7 +60,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -182,8 +182,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -59,7 +59,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -182,8 +182,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

View File

@@ -108,7 +108,7 @@ require (
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -289,8 +289,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@@ -48,6 +48,4 @@ message GetOperationDocumentRequest {
int64 started_at_unix_ms = 12;
int64 completed_at_unix_ms = 13;
string client_name = 14;
string client_address = 15;
}

View File

@@ -1,17 +1,20 @@
import 'package:money2/money2.dart';
import 'package:pshared/data/dto/money.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/utils/money.dart';
extension MoneyMapper on Money {
MoneyDTO toDTO() => MoneyDTO(
amount: amount,
currency: currency,
);
MoneyDTO toDTO() =>
MoneyDTO(amount: toDecimal().toString(), currency: currency.isoCode);
}
extension MoneyDTOMapper on MoneyDTO {
Money toDomain() => Money(
amount: amount,
currency: currency,
);
Money toDomain() {
final parsed = parseMoneyWithCurrencyCode(amount, currency);
if (parsed == null) {
throw FormatException('Invalid money dto: $currency $amount');
}
return parsed;
}
}

View File

@@ -1,18 +0,0 @@
import 'package:pshared/data/dto/wallet/chain_asset.dart';
import 'package:pshared/data/mapper/payment/enums.dart';
import 'package:pshared/models/wallet/chain_asset.dart';
extension ChainAssetDTOMapper on ChainAssetDTO {
ChainAsset toDomain() => ChainAsset(
chain: chainNetworkFromValue(chain),
tokenSymbol: tokenSymbol,
);
}
extension ChainAssetMapper on ChainAsset {
ChainAssetDTO toDTO() => ChainAssetDTO(
chain: chainNetworkToValue(chain),
tokenSymbol: tokenSymbol,
);
}

View File

@@ -1,16 +1,13 @@
import 'package:pshared/models/wallet/wallet.dart' as domain;
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
extension WalletUiMapper on domain.WalletModel {
Wallet toUi() => Wallet(
id: walletRef,
walletUserID: walletRef,
balance: parseMoneyAmount(
availableMoney?.amount ?? balance?.available?.amount,
),
balance: availableMoney?.toDouble() ?? balance?.available?.toDouble() ?? 0,
currency: currencyStringToCode(asset.tokenSymbol),
calculatedAt: balance?.calculatedAt ?? DateTime.now(),
depositAddress: depositAddress,

View File

@@ -16,8 +16,12 @@ extension WalletDTOMapper on WalletDTO {
depositAddress: depositAddress,
status: status,
metadata: metadata,
createdAt: (createdAt == null || createdAt!.isEmpty) ? null : DateTime.tryParse(createdAt!),
updatedAt: (updatedAt == null || updatedAt!.isEmpty) ? null : DateTime.tryParse(updatedAt!),
createdAt: (createdAt == null || createdAt!.isEmpty)
? null
: DateTime.tryParse(createdAt!),
updatedAt: (updatedAt == null || updatedAt!.isEmpty)
? null
: DateTime.tryParse(updatedAt!),
balance: balance?.toDomain(),
availableMoney: balance?.available?.toDomain(),
describable: newDescribable(

View File

@@ -1,18 +0,0 @@
import 'package:pshared/models/currency.dart';
import 'package:pshared/utils/currency.dart';
class Asset {
final Currency currency;
final double amount;
const Asset({
required this.currency,
required this.amount,
});
}
Asset createAsset(String currencyCode, String amount) => Asset(
currency: currencyStringToCode(currencyCode),
amount: double.parse(amount),
);

View File

@@ -1 +1 @@
enum Currency {usd, eur, rub, usdt, usdc}
enum CurrencyCode {usd, eur, rub, usdt, usdc}

View File

@@ -1,4 +1,4 @@
import 'package:pshared/models/money.dart';
import 'package:money2/money2.dart';
class LedgerBalance {

View File

@@ -1,9 +0,0 @@
class Money {
final String amount;
final String currency;
const Money({
required this.amount,
required this.currency,
});
}

View File

@@ -1,4 +1,4 @@
import 'package:pshared/models/money.dart';
import 'package:money2/money2.dart';
class PaymentExecutionOperation {

View File

@@ -1,4 +1,4 @@
import 'package:pshared/models/money.dart';
import 'package:money2/money2.dart';
class FeeLine {

View File

@@ -1,4 +1,5 @@
import 'package:pshared/models/money.dart';
import 'package:money2/money2.dart';
class FxQuote {
final String? quoteRef;

View File

@@ -1,11 +1,13 @@
import 'package:money2/money2.dart';
import 'package:pshared/models/payment/fees/treatment.dart';
import 'package:pshared/models/payment/fx/intent.dart';
import 'package:pshared/models/payment/kind.dart';
import 'package:pshared/models/payment/customer.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/models/payment/settlement_mode.dart';
class PaymentIntent {
final PaymentKind kind;
final String? sourceRef;

View File

@@ -1,4 +1,5 @@
import 'package:pshared/models/money.dart';
import 'package:money2/money2.dart';
class QuoteAmounts {
final Money? sourcePrincipal;

View File

@@ -7,7 +7,7 @@ class Wallet implements Describable {
final String id;
final String walletUserID; // ID or number that we show the user
final double balance;
final Currency currency;
final CurrencyCode currency;
final DateTime calculatedAt;
final String? depositAddress;
final ChainNetwork? network;

View File

@@ -1,12 +1,14 @@
import 'package:pshared/models/wallet/chain_asset.dart';
import 'package:pshared/models/payment/chain_network.dart';
class WalletAsset extends ChainAsset {
class WalletAsset {
final ChainNetwork chain;
final String tokenSymbol;
final String contractAddress;
const WalletAsset({
required super.chain,
required super.tokenSymbol,
required this.chain,
required this.tokenSymbol,
required this.contractAddress,
});
}

View File

@@ -1,4 +1,4 @@
import 'package:pshared/models/money.dart';
import 'package:money2/money2.dart';
class WalletBalance {

View File

@@ -1,12 +0,0 @@
import 'package:pshared/models/payment/chain_network.dart';
class ChainAsset {
final ChainNetwork chain;
final String tokenSymbol;
const ChainAsset({
required this.chain,
required this.tokenSymbol,
});
}

View File

@@ -1,5 +1,5 @@
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/money.dart';
import 'package:money2/money2.dart';
import 'package:pshared/models/wallet/asset.dart';
import 'package:pshared/models/wallet/balance.dart';
@@ -39,9 +39,7 @@ class WalletModel implements Describable {
required this.describable,
});
WalletModel copyWith({
Describable? describable,
}) => WalletModel(
WalletModel copyWith({Describable? describable}) => WalletModel(
walletRef: walletRef,
organizationRef: organizationRef,
ownerRef: ownerRef,

View File

@@ -15,6 +15,7 @@ import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/ledger.dart';
import 'package:pshared/utils/exception.dart';
class LedgerAccountsProvider with ChangeNotifier {
final LedgerService _service;
OrganizationsProvider? _organizations;
@@ -179,7 +180,7 @@ class LedgerAccountsProvider with ChangeNotifier {
Future<void> create({
required Describable describable,
required Currency currency,
required CurrencyCode currency,
String? ownerRef,
}) async {
final org = _organizations;

View File

@@ -1,3 +1,5 @@
import 'package:money2/money2.dart';
import 'package:pshared/controllers/payment/source.dart';
import 'package:pshared/models/payment/asset.dart';
import 'package:pshared/models/payment/chain_network.dart';
@@ -13,19 +15,18 @@ import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/ledger.dart';
import 'package:pshared/models/payment/methods/managed_wallet.dart';
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/models/payment/settlement_mode.dart';
import 'package:pshared/models/payment/intent.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pshared/provider/payment/flow.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/payment/fx_helpers.dart';
class QuotationIntentBuilder {
static const String _settlementCurrency = 'RUB';
static const String _addressBookCustomerFallbackId = 'address_book_customer';
PaymentIntent? build({
required PaymentAmountProvider payment,
@@ -38,10 +39,9 @@ class QuotationIntentBuilder {
final paymentData = flow.selectedPaymentData;
final selectedMethod = flow.selectedMethod;
final amountValue = payment.amount;
if (sourceMethod == null || sourceCurrency == null || paymentData == null) {
if (sourceCurrency == null) {
return null;
}
if (amountValue == null) return null;
final customer = _buildCustomer(
recipient: recipients.currentObject,
@@ -51,22 +51,22 @@ class QuotationIntentBuilder {
final amountCurrency = payment.settlementMode == SettlementMode.fixReceived
? _settlementCurrency
: sourceCurrency;
final amount = Money(
amount: amountValue.toString(),
currency: amountCurrency,
);
final currency = money2CurrencyFromCode(amountCurrency);
if (currency == null) return null;
final amount = amountValue == null
? null
: Money.fromNumWithCurrency(amountValue, currency);
final isLedgerSource = source.selectedLedgerAccount != null;
final isCryptoToCrypto =
paymentData is CryptoAddressPaymentMethod &&
(paymentData.asset?.tokenSymbol ?? '').trim().toUpperCase() ==
amount.currency;
sourceCurrency.trim().toUpperCase();
final fxIntent = _buildFxIntent(
sourceCurrency: sourceCurrency,
settlementMode: payment.settlementMode,
isLedgerSource: isLedgerSource,
enabled: !isCryptoToCrypto,
);
final comment = _resolveComment(payment.comment);
return PaymentIntent(
kind: PaymentKind.payout,
amount: amount,
@@ -77,7 +77,7 @@ class QuotationIntentBuilder {
? FeeTreatment.addToSource
: FeeTreatment.deductFromDestination,
settlementMode: payment.settlementMode,
comment: comment,
comment: payment.comment,
customer: customer,
);
}
@@ -94,14 +94,9 @@ class QuotationIntentBuilder {
// BFF maps only settlement currency + fx side, then quotation derives pair.
// For ledger this preserves source debit in ledger currency (e.g. USDT).
if (isLedgerSource && settlementMode == SettlementMode.fixReceived) {
final base = sourceCurrency.trim();
final quote = _settlementCurrency;
if (base.isEmpty || base.toUpperCase() == quote.toUpperCase()) {
return null;
}
return FxIntent(
pair: CurrencyPair(base: base, quote: quote),
side: FxSide.sellBaseBuyQuote,
pair: CurrencyPair(base: sourceCurrency, quote: _settlementCurrency),
side: FxSide.buyBaseSellQuote,
);
}
@@ -137,39 +132,59 @@ class QuotationIntentBuilder {
required PaymentMethod? method,
required PaymentMethodData? data,
}) {
final name = recipient?.name.trim();
if (name == null || name.isEmpty) return null;
final id = recipient?.id.trim();
final customerId = id == null || id.isEmpty
? _addressBookCustomerFallbackId
: id;
final customerId = recipient?.id.trim() ?? '';
final card = _resolveCard(method: method, data: data);
final fromRecipient = _buildCustomerFromName(
customerId: customerId,
fullName: recipient?.name,
country: card?.country,
);
if (fromRecipient != null) return fromRecipient;
final parts = name.split(RegExp(r'\s+'));
if (card != null) {
final firstName = _normalizedOrNull(card.firstName);
final lastName = _normalizedOrNull(card.lastName);
if (firstName == null && lastName == null) return null;
return Customer(
id: customerId,
firstName: firstName,
lastName: lastName,
country: card.country,
);
}
return null;
}
CardPaymentMethod? _resolveCard({
required PaymentMethod? method,
required PaymentMethodData? data,
}) => method?.cardData ?? (data is CardPaymentMethod ? data : null);
Customer? _buildCustomerFromName({
required String customerId,
required String? fullName,
String? country,
}) {
final normalizedName = _normalizedOrNull(fullName);
if (normalizedName == null) return null;
final parts = normalizedName.split(RegExp(r'\s+'));
final firstName = parts.isNotEmpty ? parts.first : null;
final lastName = parts.length >= 2 ? parts.last : null;
final middleName = parts.length > 2
? parts.sublist(1, parts.length - 1).join(' ')
: null;
return Customer(
id: customerId,
firstName: firstName,
middleName: middleName,
lastName: lastName,
country: _resolveCustomerCountry(method: method, data: data),
country: country,
);
}
String? _resolveCustomerCountry({
required PaymentMethod? method,
required PaymentMethodData? data,
}) {
final card = method?.cardData ?? (data is CardPaymentMethod ? data : null);
return card?.country;
}
String? _resolveComment(String comment) {
final normalized = comment.trim();
String? _normalizedOrNull(String? value) {
if (value == null) return null;
final normalized = value.trim();
return normalized.isEmpty ? null : normalized;
}
}

View File

@@ -6,13 +6,13 @@ import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart';
import 'package:money2/money2.dart';
import 'package:pshared/api/requests/payment/quote.dart';
import 'package:pshared/controllers/payment/source.dart';
import 'package:pshared/data/mapper/payment/intent/payment.dart';
import 'package:pshared/models/asset.dart';
import 'package:pshared/models/payment/intent.dart';
import 'package:pshared/models/payment/quote/quote.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/models/auto_refresh_mode.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/payment/amount.dart';
@@ -79,20 +79,12 @@ class QuotationProvider extends ChangeNotifier {
return DateTime.fromMillisecondsSinceEpoch(expiresAtUnixMs, isUtc: true);
}
Asset? get fee => _assetFromMoney(quoteFeeTotal(quotation));
Asset? get total => _assetFromMoney(
quoteSourceDebitTotal(
Money? get fee => quoteFeeTotal(quotation);
Money? get total => quoteSourceDebitTotal(
quotation,
preferredSourceCurrency: _sourceCurrencyCode,
),
);
Asset? get recipientGets =>
_assetFromMoney(quotation?.amounts?.destinationSettlement);
Asset? _assetFromMoney(Money? money) {
if (money == null) return null;
return createAsset(money.currency, money.amount);
}
Money? get recipientGets => quotation?.amounts?.destinationSettlement;
void _setResource(Resource<PaymentQuote> quotation) {
_quotation = quotation;

View File

@@ -5,14 +5,16 @@ import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import 'package:pshared/models/currency.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/payment/chain_network.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/models/wallet/chain_asset.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/payment/wallets.dart';
import 'package:pshared/utils/exception.dart';
class WalletsProvider with ChangeNotifier {
final WalletsService _service;
OrganizationsProvider? _organizations;
@@ -180,7 +182,8 @@ class WalletsProvider with ChangeNotifier {
Future<void> create({
required Describable describable,
required ChainAsset asset,
required ChainNetwork chain,
required CurrencyCode currency,
required String? ownerRef,
}) async {
final org = _organizations;
@@ -195,7 +198,8 @@ class WalletsProvider with ChangeNotifier {
await _service.create(
organizationRef: org.current.id,
describable: describable,
asset: asset,
chain: chain,
currency: currency,
ownerRef: ownerRef,
);
await loadWalletsWithBalances();

View File

@@ -43,7 +43,7 @@ class LedgerService {
required String organizationRef,
required Describable describable,
required String? ownerRef,
required Currency currency,
required CurrencyCode currency,
}) async => AuthorizationService.getPOSTResponse(
_objectType,
'/$organizationRef',

View File

@@ -13,12 +13,10 @@ class PaymentDocumentsService {
String organizationRef,
String gatewayService,
String operationRef,
String paymentRef,
) async {
final query = <String, String>{
'gateway_service': gatewayService,
'operation_ref': operationRef,
'payment_ref': paymentRef,
};
final queryString = Uri(queryParameters: query).query;
final url = '/documents/operation/$organizationRef?$queryString';

View File

@@ -1,9 +1,9 @@
import 'package:pshared/data/mapper/wallet/ui.dart';
import 'package:pshared/models/currency.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/payment/chain_network.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/models/wallet/chain_asset.dart';
import 'package:pshared/service/wallet.dart' as shared_wallet_service;
import 'package:pshared/utils/money.dart';
abstract class WalletsService {
@@ -12,7 +12,8 @@ abstract class WalletsService {
Future<void> create({
required String organizationRef,
required Describable describable,
required ChainAsset asset,
required ChainNetwork chain,
required CurrencyCode currency,
required String? ownerRef,
});
}
@@ -30,19 +31,21 @@ class ApiWalletsService implements WalletsService {
organizationRef: organizationRef,
walletRef: walletRef,
);
return parseMoneyAmount(balance.available?.amount);
return balance.available?.toDouble() ?? 0;
}
@override
Future<void> create({
required String organizationRef,
required Describable describable,
required ChainAsset asset,
required ChainNetwork chain,
required CurrencyCode currency,
required String? ownerRef,
}) => shared_wallet_service.WalletService.create(
organizationRef: organizationRef,
describable: describable,
asset: asset,
chain: chain,
currency: currency,
ownerRef: ownerRef,
);
}

View File

@@ -1,15 +1,18 @@
import 'package:pshared/api/requests/wallet/create.dart';
import 'package:pshared/api/responses/wallet_balance.dart';
import 'package:pshared/api/responses/wallets.dart';
import 'package:pshared/data/dto/wallet/chain_asset.dart';
import 'package:pshared/data/mapper/describable.dart';
import 'package:pshared/data/mapper/wallet/chain_asset.dart';
import 'package:pshared/data/mapper/payment/enums.dart';
import 'package:pshared/data/mapper/wallet/response.dart';
import 'package:pshared/models/currency.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/payment/chain_network.dart';
import 'package:pshared/models/wallet/balance.dart';
import 'package:pshared/models/wallet/chain_asset.dart';
import 'package:pshared/models/wallet/wallet.dart';
import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/services.dart';
import 'package:pshared/utils/currency.dart';
class WalletService {
@@ -37,13 +40,19 @@ class WalletService {
static Future<void> create({
required String organizationRef,
required Describable describable,
required ChainAsset asset,
required ChainNetwork chain,
required CurrencyCode currency,
required String? ownerRef,
}) async => AuthorizationService.getPOSTResponse(
_objectType,
'/$organizationRef',
CreateWalletRequest(
asset: asset.toDTO(),
asset: ChainAssetDTO(
chain: chainNetworkToValue(chain),
tokenSymbol:
money2CurrencyFromCode(currencyCodeToString(currency))?.isoCode ??
currencyCodeToString(currency),
),
describable: describable.toDTO(),
ownerRef: ownerRef,
).toJson(),

View File

@@ -1,102 +1,78 @@
import 'package:flutter/material.dart';
import 'package:money2/money2.dart';
import 'package:pshared/models/asset.dart';
import 'package:pshared/models/currency.dart';
const nonBreakingSpace = '\u00A0';
final Currency _usdtCurrency = Currency.create(
'USDT',
6,
symbol: '',
isIso: false,
country: 'Digital',
unit: 'Tether',
name: 'Tether',
);
String withTrailingNonBreakingSpace(String value) {
return '$value$nonBreakingSpace';
}
final Currency _usdcCurrency = Currency.create(
'USDC',
6,
symbol: r'($)',
isIso: false,
country: 'Digital',
unit: 'USD Coin',
name: 'USD Coin',
);
String joinWithNonBreakingSpace(String left, String right) {
return '$left$nonBreakingSpace$right';
}
final Map<String, Currency> _commonCurrenciesByCode =
<String, Currency>{
for (final currency in CommonCurrencies().asList())
currency.isoCode: currency,
_usdtCurrency.isoCode: _usdtCurrency,
_usdcCurrency.isoCode: _usdcCurrency,
};
String currencyCodeToSymbol(Currency currencyCode) {
switch (currencyCode) {
case Currency.usd:
return '\$';
case Currency.usdt:
return '';
case Currency.usdc:
return '\$';
case Currency.rub:
return '';
case Currency.eur:
return '';
String currencyCodeToSymbol(CurrencyCode currencyCode) {
final symbol = currencySymbolFromCode(currencyCodeToString(currencyCode));
if (symbol == null || symbol.trim().isEmpty) {
return currencyCodeToString(currencyCode);
}
return symbol;
}
String amountToString(double amount) {
return amount.toStringAsFixed(2);
String currencyToString(CurrencyCode currencyCode, double amount) {
final code = currencyCodeToString(currencyCode);
final currency = money2CurrencyFromCode(code);
if (currency == null) {
return '$amount $code';
}
final money = Money.fromNumWithCurrency(amount, currency);
return money.toString();
}
String currencyToString(Currency currencyCode, double amount) {
return joinWithNonBreakingSpace(
currencyCodeToSymbol(currencyCode),
amountToString(amount),
);
}
CurrencyCode currencyStringToCode(String currencyCode) {
final normalized = currencyCode.trim().toUpperCase();
for (final value in CurrencyCode.values) {
if (currencyCodeToString(value) == normalized) {
return value;
}
}
String assetToString(Asset asset) {
return currencyToString(asset.currency, asset.amount);
}
Currency currencyStringToCode(String currencyCode) {
switch (currencyCode) {
case 'USD':
return Currency.usd;
case 'USDT':
return Currency.usdt;
case 'USDC':
return Currency.usdc;
case 'RUB':
return Currency.rub;
case 'EUR':
return Currency.eur;
default:
throw ArgumentError('Unknown currency code: $currencyCode');
}
}
String currencyCodeToString(Currency currencyCode) {
switch (currencyCode) {
case Currency.usd:
return 'USD';
case Currency.usdt:
return 'USDT';
case Currency.usdc:
return 'USDC';
case Currency.rub:
return 'RUB';
case Currency.eur:
return 'EUR';
}
}
IconData iconForCurrencyType(Currency currencyCode) {
switch (currencyCode) {
case Currency.usd:
return Icons.currency_exchange;
case Currency.eur:
return Icons.currency_exchange;
case Currency.rub:
return Icons.currency_ruble;
case Currency.usdt:
return Icons.currency_exchange;
case Currency.usdc:
return Icons.money;
}
String currencyCodeToString(CurrencyCode currencyCode) {
return currencyCode.name.toUpperCase();
}
String? currencySymbolFromCode(String? code) {
final normalized = code?.trim();
if (normalized == null || normalized.isEmpty) return null;
try {
return currencyCodeToSymbol(currencyStringToCode(normalized.toUpperCase()));
} catch (_) {
return null;
}
final currency = money2CurrencyFromCode(code);
if (currency == null) return null;
final symbol = currency.symbol.trim();
return symbol.isEmpty ? null : symbol;
}
Currency? money2CurrencyFromCode(String? code) {
final normalized = code?.trim().toUpperCase();
if (normalized == null || normalized.isEmpty) return null;
return _commonCurrenciesByCode[normalized] ?? Currencies().find(normalized);
}

View File

@@ -1,41 +1,35 @@
import 'package:pshared/models/money.dart';
import 'package:money2/money2.dart';
import 'package:pshared/utils/currency.dart';
const String _decimalMoneyPattern = '0.##################';
double parseMoneyAmount(String? raw, {double fallback = 0}) {
final trimmed = raw?.trim();
if (trimmed == null || trimmed.isEmpty) return fallback;
return double.tryParse(trimmed) ?? fallback;
Money? parseMoneyWithCurrency(String? amount, Currency? currency) {
if (currency == null) return null;
final value = _normalizeMoneyAmount(amount);
if (value == null || value.isEmpty) return null;
try {
return Money.parseWithCurrency(
value,
currency,
pattern: _decimalMoneyPattern,
);
} catch (_) {
return null;
}
}
String formatMoneyDisplay(
Money? money, {
String fallback = '--',
String separator = ' ',
String invalidAmountFallback = '',
}) {
if (money == null) return fallback;
final rawAmount = money.amount.trim();
final rawCurrency = money.currency.trim();
final parsedAmount = parseMoneyAmount(rawAmount, fallback: double.nan);
final amountToken = parsedAmount.isNaN
? (rawAmount.isEmpty ? invalidAmountFallback : rawAmount)
: amountToString(parsedAmount);
final symbol = currencySymbolFromCode(rawCurrency);
final normalizedSymbol = symbol?.trim() ?? '';
final hasSymbol = normalizedSymbol.isNotEmpty;
final currencyToken = hasSymbol ? normalizedSymbol : rawCurrency;
final first = amountToken;
final second = currencyToken;
if (first.isEmpty && second.isEmpty) return fallback;
if (first.isEmpty) return second;
if (second.isEmpty) return first;
return '$first$separator$second';
Money? parseMoneyWithCurrencyCode(String? amount, String? currencyCode) {
return parseMoneyWithCurrency(amount, money2CurrencyFromCode(currencyCode));
}
extension MoneyAmountX on Money {
double get amountValue => parseMoneyAmount(amount);
String? _normalizeMoneyAmount(String? value) {
final normalized = value?.trim();
if (normalized == null || normalized.isEmpty) return null;
if (normalized.contains(',') && !normalized.contains('.')) {
return normalized.replaceAll(',', '.');
}
return normalized;
}

View File

@@ -1,7 +1,8 @@
import 'package:money2/money2.dart';
import 'package:pshared/models/payment/currency_pair.dart';
import 'package:pshared/models/payment/fx/intent.dart';
import 'package:pshared/models/payment/fx/side.dart';
import 'package:pshared/models/money.dart';
class FxIntentHelper {
@@ -37,11 +38,15 @@ class FxIntentHelper {
case FxSide.unspecified:
break;
}
if (amount.currency == pair.base && pair.quote.isNotEmpty) return pair.quote;
if (amount.currency == pair.quote && pair.base.isNotEmpty) return pair.base;
if (amount.currency.isoCode == pair.base && pair.quote.isNotEmpty) {
return pair.quote;
}
if (amount.currency.isoCode == pair.quote && pair.base.isNotEmpty) {
return pair.base;
}
if (pair.quote.isNotEmpty) return pair.quote;
if (pair.base.isNotEmpty) return pair.base;
}
return amount.currency;
return amount.currency.isoCode;
}
}

View File

@@ -12,7 +12,7 @@ class PaymentQuotationCurrencyResolver {
PaymentMethodData? paymentData,
}) {
final quoteCurrency = _normalizeCurrency(
quote?.amounts?.destinationSettlement?.currency,
quote?.amounts?.destinationSettlement?.currency.isoCode,
);
if (quoteCurrency != null) return quoteCurrency;

View File

@@ -1,17 +1,16 @@
import 'package:pshared/models/money.dart';
import 'package:money2/money2.dart';
import 'package:pshared/models/payment/fees/line.dart';
import 'package:pshared/models/payment/quote/quote.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
Money? quoteFeeTotal(PaymentQuote? quote) {
final preferredCurrency =
quote?.amounts?.sourcePrincipal?.currency ??
quote?.amounts?.sourceDebitTotal?.currency;
return quoteFeeTotalFromLines(
quote?.fees?.lines,
preferredCurrency: preferredCurrency,
preferredCurrency:
quote?.amounts?.sourcePrincipal?.currency.isoCode ??
quote?.amounts?.sourceDebitTotal?.currency.isoCode,
);
}
@@ -21,7 +20,8 @@ Money? quoteSourceDebitTotal(
}) {
final sourceDebitTotal = quote?.amounts?.sourceDebitTotal;
final preferredCurrency = _normalizeCurrency(
preferredSourceCurrency ?? quote?.amounts?.sourcePrincipal?.currency,
preferredSourceCurrency ??
quote?.amounts?.sourcePrincipal?.currency.isoCode,
);
if (sourceDebitTotal == null) {
@@ -31,10 +31,9 @@ Money? quoteSourceDebitTotal(
);
}
final debitCurrency = _normalizeCurrency(sourceDebitTotal.currency);
if (preferredCurrency == null ||
debitCurrency == null ||
debitCurrency == preferredCurrency) {
_normalizeCurrency(sourceDebitTotal.currency.isoCode) ==
preferredCurrency) {
return sourceDebitTotal;
}
@@ -52,22 +51,18 @@ Money? quoteFeeTotalFromLines(
if (lines == null || lines.isEmpty) return null;
final normalizedPreferred = _normalizeCurrency(preferredCurrency);
final totalsByCurrency = <String, double>{};
final totalsByCurrency = <String, Money>{};
for (final line in lines) {
final money = line.amount;
if (money == null) continue;
final parsedAmount = line.amount;
if (parsedAmount == null) continue;
final currency = _normalizeCurrency(money.currency);
if (currency == null) continue;
final amount = parseMoneyAmount(money.amount, fallback: double.nan);
if (amount.isNaN) continue;
final sign = _lineSign(line.side);
final signedAmount = sign * amount.abs();
totalsByCurrency[currency] =
(totalsByCurrency[currency] ?? 0) + signedAmount;
final currencyCode = parsedAmount.currency.isoCode;
final signedAmount = _isCreditLine(line.side) ? -parsedAmount : parsedAmount;
final current = totalsByCurrency[currencyCode];
totalsByCurrency[currencyCode] = current == null
? signedAmount
: current + signedAmount;
}
if (totalsByCurrency.isEmpty) return null;
@@ -77,85 +72,59 @@ Money? quoteFeeTotalFromLines(
totalsByCurrency.containsKey(normalizedPreferred)
? normalizedPreferred
: totalsByCurrency.keys.first;
final total = totalsByCurrency[selectedCurrency];
if (total == null) return null;
return Money(amount: amountToString(total), currency: selectedCurrency);
return totalsByCurrency[selectedCurrency];
}
List<Money> aggregateMoneyByCurrency(Iterable<Money?> values) {
final totals = <String, double>{};
final totals = <String, Money>{};
for (final value in values) {
if (value == null) continue;
final currency = _normalizeCurrency(value.currency);
if (currency == null) continue;
final amount = parseMoneyAmount(value.amount, fallback: double.nan);
if (amount.isNaN) continue;
totals[currency] = (totals[currency] ?? 0) + amount;
final currency = value.currency.isoCode;
final current = totals[currency];
totals[currency] = current == null ? value : current + value;
}
return totals.entries
.map(
(entry) =>
Money(amount: amountToString(entry.value), currency: entry.key),
)
.toList();
return totals.values.toList();
}
Money? _rebuildSourceDebitTotal(
PaymentQuote? quote, {
String? preferredSourceCurrency,
}) {
final sourcePrincipal = quote?.amounts?.sourcePrincipal;
if (sourcePrincipal == null) return null;
final principal = quote?.amounts?.sourcePrincipal;
if (principal == null) return null;
final principalCurrency = _normalizeCurrency(sourcePrincipal.currency);
if (principalCurrency == null) return null;
final principalCurrency = principal.currency.isoCode;
if (preferredSourceCurrency != null &&
principalCurrency != preferredSourceCurrency) {
return null;
}
final principalAmount = parseMoneyAmount(
sourcePrincipal.amount,
fallback: double.nan,
);
if (principalAmount.isNaN) return null;
double totalAmount = principalAmount;
var totalAmount = principal;
final fee = quoteFeeTotalFromLines(
quote?.fees?.lines,
preferredCurrency: principalCurrency,
);
if (fee != null && _normalizeCurrency(fee.currency) == principalCurrency) {
final feeAmount = parseMoneyAmount(fee.amount, fallback: double.nan);
if (!feeAmount.isNaN) {
totalAmount += feeAmount;
}
if (fee != null && fee.currency.isoCode == principalCurrency) {
totalAmount += fee;
}
return Money(
amount: amountToString(totalAmount),
currency: principalCurrency,
);
return totalAmount;
}
double _lineSign(String? side) {
bool _isCreditLine(String? side) {
final normalized = side?.trim().toLowerCase() ?? '';
switch (normalized) {
case 'entry_side_credit':
case 'credit':
return -1;
return true;
default:
return 1;
return false;
}
}
String? _normalizeCurrency(String? currency) {
final normalized = currency?.trim().toUpperCase();
final normalized = currency?.trim();
if (normalized == null || normalized.isEmpty) return null;
return normalized;
return money2CurrencyFromCode(normalized)?.isoCode ?? normalized.toUpperCase();
}

View File

@@ -28,6 +28,7 @@ dependencies:
uuid: ^4.5.1
image: ^4.5.4
shared_preferences: ^2.5.3
money2: ^6.3.0
dev_dependencies:
flutter_lints: ^6.0.0

View File

@@ -348,7 +348,7 @@ RouteBase payoutShellRoute() => ShellRoute(
path: PayoutRoutes.reportPaymentPath,
pageBuilder: (_, state) => NoTransitionPage(
child: PaymentDetailsPage(
paymentRef:
paymentId:
state.uri.queryParameters[PayoutRoutes.reportPaymentIdQuery] ??
'',
),

View File

@@ -1,102 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/status.dart';
import 'package:pweb/models/wallet/wallet_transaction.dart';
import 'package:pweb/providers/wallet_transactions.dart';
class WalletTransactionsController extends ChangeNotifier {
List<WalletTransaction> _filteredTransactions = [];
DateTimeRange? _dateRange;
final Set<OperationStatus> _selectedStatuses = {};
final Set<WalletTransactionType> _selectedTypes = {};
WalletTransactionsProvider? _provider;
List<WalletTransaction> get transactions =>
_provider?.transactions ?? const [];
List<WalletTransaction> get filteredTransactions => _filteredTransactions;
DateTimeRange? get dateRange => _dateRange;
Set<OperationStatus> get selectedStatuses => _selectedStatuses;
Set<WalletTransactionType> get selectedTypes => _selectedTypes;
bool get isLoading => _provider?.isLoading ?? false;
String? get error => _provider?.error;
bool get hasFilters =>
_dateRange != null ||
_selectedStatuses.isNotEmpty ||
_selectedTypes.isNotEmpty;
void update(WalletTransactionsProvider provider) {
if (identical(_provider, provider)) return;
_provider?.removeListener(_onProviderChanged);
_provider = provider;
_provider?.addListener(_onProviderChanged);
_rebuildFiltered(notify: false);
notifyListeners();
}
void setDateRange(DateTimeRange? range) {
_dateRange = range;
_rebuildFiltered();
}
void toggleStatus(OperationStatus status) {
if (_selectedStatuses.contains(status)) {
_selectedStatuses.remove(status);
} else {
_selectedStatuses.add(status);
}
_rebuildFiltered();
}
void toggleType(WalletTransactionType type) {
if (_selectedTypes.contains(type)) {
_selectedTypes.remove(type);
} else {
_selectedTypes.add(type);
}
_rebuildFiltered();
}
void resetFilters() {
_dateRange = null;
_selectedStatuses.clear();
_selectedTypes.clear();
_rebuildFiltered();
}
void _onProviderChanged() {
_rebuildFiltered();
}
void _rebuildFiltered({bool notify = true}) {
final source = _provider?.transactions ?? const <WalletTransaction>[];
final activeWalletId = _provider?.walletId;
_filteredTransactions = source.where((tx) {
final walletMatch =
activeWalletId == null || tx.walletId == activeWalletId;
final statusMatch =
_selectedStatuses.isEmpty || _selectedStatuses.contains(tx.status);
final typeMatch =
_selectedTypes.isEmpty || _selectedTypes.contains(tx.type);
final dateMatch =
_dateRange == null ||
(tx.date.isAfter(
_dateRange!.start.subtract(const Duration(seconds: 1)),
) &&
tx.date.isBefore(
_dateRange!.end.add(const Duration(seconds: 1)),
));
return walletMatch && statusMatch && typeMatch && dateMatch;
}).toList();
if (notify) notifyListeners();
}
@override
void dispose() {
_provider?.removeListener(_onProviderChanged);
super.dispose();
}
}

View File

@@ -1,10 +1,11 @@
import 'package:money2/money2.dart';
import 'package:flutter/material.dart';
import 'package:pshared/controllers/payment/source.dart';
import 'package:pshared/models/payment/settlement_mode.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
import 'package:pweb/models/payment/amount/mode.dart';
@@ -22,7 +23,7 @@ class PaymentAmountFieldController extends ChangeNotifier {
PaymentAmountFieldController({required double? initialAmount})
: textController = TextEditingController(
text: initialAmount == null ? '' : amountToString(initialAmount),
text: initialAmount == null ? '' : initialAmount.toString(),
);
PaymentAmountMode get mode => _mode;
@@ -122,18 +123,14 @@ class PaymentAmountFieldController extends ChangeNotifier {
};
double? _parseAmount(String value) {
final parsed = parseMoneyAmount(
value.replaceAll(',', '.'),
fallback: double.nan,
);
return parsed.isNaN ? null : parsed;
return double.tryParse(value.replaceAll(',', '.').trim());
}
void _syncTextWithAmount(double? amount) {
final parsedText = _parseAmount(textController.text);
if (parsedText == amount) return;
final nextText = amount == null ? '' : amountToString(amount);
final nextText = amount == null ? '' : _formatAmount(amount);
_isSyncingText = true;
textController.value = TextEditingValue(
text: nextText,
@@ -142,6 +139,12 @@ class PaymentAmountFieldController extends ChangeNotifier {
_isSyncingText = false;
}
String _formatAmount(double amount) {
final currency = money2CurrencyFromCode(activeCurrencyCode);
if (currency == null) return amount.toString();
return Money.fromNumWithCurrency(amount, currency).toDecimal().toString();
}
@override
void dispose() {
_provider?.removeListener(_handleProviderChanged);

View File

@@ -9,14 +9,14 @@ import 'package:pweb/utils/report/operations/document_rule.dart';
class PaymentDetailsController extends ChangeNotifier {
PaymentDetailsController({required String paymentRef})
: _paymentRef = paymentRef;
PaymentDetailsController({required String paymentId})
: _paymentId = paymentId;
PaymentsProvider? _payments;
String _paymentRef;
String _paymentId;
Payment? _payment;
String get paymentId => _paymentRef;
String get paymentId => _paymentId;
Payment? get payment => _payment;
bool get isLoading => _payments?.isLoading ?? false;
Exception? get error => _payments?.error;
@@ -44,8 +44,8 @@ class PaymentDetailsController extends ChangeNotifier {
operationDocumentRequest(operation) != null;
void update(PaymentsProvider provider, String paymentId) {
if (_paymentRef != paymentId) {
_paymentRef = paymentId;
if (_paymentId != paymentId) {
_paymentId = paymentId;
}
if (!identical(_payments, provider)) {
@@ -60,7 +60,7 @@ class PaymentDetailsController extends ChangeNotifier {
}
void _rebuild() {
_payment = _findPayment(_payments?.payments ?? const [], _paymentRef);
_payment = _findPayment(_payments?.payments ?? const [], _paymentId);
notifyListeners();
}

View File

@@ -29,5 +29,4 @@ class RecentPaymentsController extends ChangeNotifier {
_recent = sortOperations(operations).take(5).toList();
notifyListeners();
}
}

View File

@@ -2,8 +2,9 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:money2/money2.dart';
import 'package:pshared/controllers/payment/source.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/models/payment/asset.dart';
import 'package:pshared/models/payment/chain_network.dart';
import 'package:pshared/models/payment/methods/data.dart';
@@ -17,6 +18,7 @@ import 'package:pweb/models/payment/multiple_payouts/state.dart';
import 'package:pweb/providers/multiple_payouts.dart';
import 'package:pweb/services/payments/csv_input.dart';
class MultiplePayoutsController extends ChangeNotifier {
final CsvInputService _csvInput;
MultiplePayoutsProvider? _provider;

View File

@@ -31,10 +31,7 @@ import 'package:pweb/app/app.dart';
import 'package:pweb/pages/invitations/widgets/list/view_model.dart';
import 'package:pweb/app/timeago.dart';
import 'package:pweb/providers/two_factor.dart';
import 'package:pweb/controllers/operations/wallet_transactions.dart';
import 'package:pweb/providers/wallet_transactions.dart';
import 'package:pweb/services/posthog.dart';
import 'package:pweb/services/wallet_transactions.dart';
import 'package:pweb/providers/account.dart';
import 'package:pweb/providers/locale.dart';
@@ -142,18 +139,6 @@ void main() async {
create: (_) => WalletsController(),
update: (_, wallets, controller) => controller!..update(wallets),
),
ChangeNotifierProvider(
create: (_) => WalletTransactionsProvider(
MockWalletTransactionsService(),
),
),
ChangeNotifierProxyProvider<
WalletTransactionsProvider,
WalletTransactionsController
>(
create: (_) => WalletTransactionsController(),
update: (_, provider, controller) => controller!..update(provider),
),
],
child: const PayApp(),
),

View File

@@ -29,7 +29,7 @@ class WalletTransaction {
final WalletTransactionType type;
final OperationStatus status;
final double amount;
final Currency currency;
final CurrencyCode currency;
final DateTime date;
final String description;
final String? counterparty;
@@ -56,7 +56,7 @@ class WalletTransaction {
WalletTransactionType? type,
OperationStatus? status,
double? amount,
Currency? currency,
CurrencyCode? currency,
DateTime? date,
String? description,
String? counterparty,

View File

@@ -2,6 +2,6 @@ import 'package:pshared/models/currency.dart';
import 'package:pshared/models/payment/chain_network.dart';
const Currency managedCurrencyDefault = Currency.usdt;
const Currency ledgerCurrencyDefault = Currency.rub;
const CurrencyCode managedCurrencyDefault = CurrencyCode.usdt;
const CurrencyCode ledgerCurrencyDefault = CurrencyCode.rub;
const ChainNetwork managedNetworkDefault = ChainNetwork.tronMainnet;

View File

@@ -8,11 +8,9 @@ import 'package:pshared/models/currency.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/payment/chain_network.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/wallet/chain_asset.dart';
import 'package:pshared/provider/accounts/employees.dart';
import 'package:pshared/provider/ledger.dart';
import 'package:pshared/provider/payment/wallets.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/pages/dashboard/buttons/balance/add/form.dart';
import 'package:pweb/pages/dashboard/buttons/balance/add/constants.dart';
@@ -42,9 +40,9 @@ class _AddBalanceDialogState extends State<AddBalanceDialog> {
PaymentType _assetType = PaymentType.managedWallet;
String? _ownerRef;
Currency _managedCurrency = managedCurrencyDefault;
CurrencyCode _managedCurrency = managedCurrencyDefault;
ChainNetwork _network = managedNetworkDefault;
Currency _ledgerCurrency = ledgerCurrencyDefault;
CurrencyCode _ledgerCurrency = ledgerCurrencyDefault;
@override
void dispose() {
@@ -60,7 +58,7 @@ class _AddBalanceDialogState extends State<AddBalanceDialog> {
void _setOwnerRef(String? value) => setState(() => _ownerRef = value);
void _setManagedCurrency(Currency? value) {
void _setManagedCurrency(CurrencyCode? value) {
if (value == null) return;
setState(() => _managedCurrency = value);
}
@@ -70,7 +68,7 @@ class _AddBalanceDialogState extends State<AddBalanceDialog> {
setState(() => _network = value);
}
void _setLedgerCurrency(Currency? value) {
void _setLedgerCurrency(CurrencyCode? value) {
if (value == null) return;
setState(() => _ledgerCurrency = value);
}
@@ -102,7 +100,8 @@ class _AddBalanceDialogState extends State<AddBalanceDialog> {
if (_assetType == PaymentType.managedWallet) {
await context.read<WalletsProvider>().create(
describable: newDescribable(name: name, description: description),
asset: ChainAsset(chain: _network, tokenSymbol: currencyCodeToString(_managedCurrency)),
chain: _network,
currency: _managedCurrency,
ownerRef: owner?.id,
);
} else {

View File

@@ -23,12 +23,12 @@ class AddBalanceForm extends StatelessWidget {
final ValueChanged<String?> onOwnerChanged;
final TextEditingController nameController;
final TextEditingController descriptionController;
final Currency managedCurrency;
final CurrencyCode managedCurrency;
final ChainNetwork network;
final Currency ledgerCurrency;
final ValueChanged<Currency?> onManagedCurrencyChanged;
final CurrencyCode ledgerCurrency;
final ValueChanged<CurrencyCode?> onManagedCurrencyChanged;
final ValueChanged<ChainNetwork?> onNetworkChanged;
final ValueChanged<Currency?> onLedgerCurrencyChanged;
final ValueChanged<CurrencyCode?> onLedgerCurrencyChanged;
final bool showEmployeesLoading;
const AddBalanceForm({

View File

@@ -4,7 +4,7 @@ import 'package:pshared/models/currency.dart';
import 'package:pshared/utils/currency.dart';
DropdownMenuItem<Currency> currencyItem(Currency currency) => DropdownMenuItem(
DropdownMenuItem<CurrencyCode> currencyItem(CurrencyCode currency) => DropdownMenuItem(
value: currency,
child: Text(currencyCodeToString(currency)),
);

View File

@@ -10,8 +10,8 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class LedgerFields extends StatelessWidget {
final Currency currency;
final ValueChanged<Currency?>? onCurrencyChanged;
final CurrencyCode currency;
final ValueChanged<CurrencyCode?>? onCurrencyChanged;
const LedgerFields({
super.key,
@@ -20,7 +20,7 @@ class LedgerFields extends StatelessWidget {
});
@override
Widget build(BuildContext context) => DropdownButtonFormField<Currency>(
Widget build(BuildContext context) => DropdownButtonFormField<CurrencyCode>(
initialValue: currency,
decoration: getInputDecoration(context, AppLocalizations.of(context)!.currency, true),
items: [

View File

@@ -12,9 +12,9 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class ManagedWalletFields extends StatelessWidget {
final Currency currency;
final CurrencyCode currency;
final ChainNetwork network;
final ValueChanged<Currency?>? onCurrencyChanged;
final ValueChanged<CurrencyCode?>? onCurrencyChanged;
final ValueChanged<ChainNetwork?>? onNetworkChanged;
const ManagedWalletFields({
@@ -31,7 +31,7 @@ class ManagedWalletFields extends StatelessWidget {
return Column(
spacing: 12,
children: [
DropdownButtonFormField<Currency>(
DropdownButtonFormField<CurrencyCode>(
initialValue: currency,
decoration: getInputDecoration(context, l10n.currency, true),
items: [

View File

@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/utils/currency.dart';
@@ -28,12 +27,10 @@ class BalanceAmount extends StatelessWidget {
final textTheme = Theme.of(context).textTheme;
final colorScheme = Theme.of(context).colorScheme;
final currencyBalance = currencyCodeToSymbol(wallet.currency);
final formattedBalance = formatMoneyUi(
final formattedBalance = formatAmountUi(
context,
Money(
amount: amountToString(wallet.balance),
amount: wallet.balance,
currency: currencyCodeToString(wallet.currency),
),
);
final wallets = context.watch<WalletsController>();
final isMasked = wallets.isBalanceMasked(wallet.id);

View File

@@ -36,9 +36,7 @@ class PaymentAmountField extends StatelessWidget {
decoration: InputDecoration(
labelText: loc.amount,
border: const OutlineInputBorder(),
prefixText: symbol == null
? null
: withTrailingNonBreakingSpace(symbol),
prefixText: symbol == null ? null : '$symbol ',
),
onChanged: ui.handleChanged,
),

View File

@@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/models/dashboard/summary_values.dart';
import 'package:pweb/pages/dashboard/payouts/summary/widget.dart';
@@ -28,21 +26,12 @@ class SourceQuoteSummary extends StatelessWidget {
values: PaymentSummaryValues(
fee: controller.aggregateFeeAmount == null
? l10n.noFee
: formatMoneyUiWithL10n(
l10n,
controller.aggregateFeeAmount,
separator: nonBreakingSpace,
),
recipientReceives: formatMoneyUiWithL10n(
l10n,
: formatMoneyUi(context, controller.aggregateFeeAmount),
recipientReceives: formatMoneyUi(
context,
controller.aggregateSettlementAmount,
separator: nonBreakingSpace,
),
total: formatMoneyUiWithL10n(
l10n,
controller.aggregateDebitAmount,
separator: nonBreakingSpace,
),
total: formatMoneyUi(context, controller.aggregateDebitAmount),
),
);
}

View File

@@ -19,7 +19,10 @@ class UploadHistorySection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<PaymentsProvider, RecentPaymentsController>(
return ChangeNotifierProxyProvider<
PaymentsProvider,
RecentPaymentsController
>(
create: (_) => RecentPaymentsController(),
update: (_, payments, controller) => controller!..update(payments),
child: const _RecentPaymentsView(),

View File

@@ -8,7 +8,6 @@ import 'package:pweb/pages/dashboard/payouts/summary/row.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentFeeRow extends StatelessWidget {
const PaymentFeeRow({super.key});
@@ -19,7 +18,7 @@ class PaymentFeeRow extends StatelessWidget {
final l10 = AppLocalizations.of(context)!;
return PaymentSummaryRow(
labelFactory: l10.fee,
asset: fee,
money: fee,
value: fee == null ? l10.noFee : null,
style: Theme.of(context).textTheme.titleMedium,
);

View File

@@ -8,7 +8,6 @@ import 'package:pweb/pages/dashboard/payouts/summary/row.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentRecipientReceivesRow extends StatelessWidget {
const PaymentRecipientReceivesRow({super.key});
@@ -16,7 +15,7 @@ class PaymentRecipientReceivesRow extends StatelessWidget {
Widget build(BuildContext context) => Consumer<QuotationProvider>(
builder: (context, provider, _) => PaymentSummaryRow(
labelFactory: AppLocalizations.of(context)!.recipientWillReceive,
asset: provider.recipientGets,
money: provider.recipientGets,
style: Theme.of(context).textTheme.titleMedium,
),
);

View File

@@ -1,27 +1,25 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/asset.dart';
import 'package:money2/money2.dart';
import 'package:pweb/utils/money_display.dart';
class PaymentSummaryRow extends StatelessWidget {
final String Function(String) labelFactory;
final Asset? asset;
final Money? money;
final String? value;
final TextStyle? style;
const PaymentSummaryRow({
super.key,
required this.labelFactory,
required this.asset,
required this.money,
this.value,
this.style,
});
@override
Widget build(BuildContext context) {
final formatted = value ?? formatAssetUi(context, asset);
final formatted = value ?? formatMoneyUi(context, money);
return Text(labelFactory(formatted), style: style);
}
}

View File

@@ -8,7 +8,6 @@ import 'package:pweb/pages/dashboard/payouts/summary/row.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentTotalRow extends StatelessWidget {
const PaymentTotalRow({super.key});
@@ -16,8 +15,10 @@ class PaymentTotalRow extends StatelessWidget {
Widget build(BuildContext context) => Consumer<QuotationProvider>(
builder: (context, provider, _) => PaymentSummaryRow(
labelFactory: AppLocalizations.of(context)!.total,
asset: provider.total,
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600),
money: provider.total,
style: Theme.of(
context,
).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600),
),
);
}

Some files were not shown because too many files have changed in this diff Show More