Compare commits
53 Commits
d07e64fc4f
...
SEND010
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ecd17d2dc | ||
| d649748f6f | |||
|
|
61177a4e30 | ||
| c7b9b70d57 | |||
|
|
5030453807 | ||
| 5565081b69 | |||
| b216aa68b7 | |||
| 7ac1c519e3 | |||
| 076b0c6434 | |||
|
|
9a90e6a03b | ||
|
|
5218632c00 | ||
|
|
a2c05745ad | ||
|
|
82b2f88122 | ||
|
|
28d74d058b | ||
|
|
6ee146b95a | ||
| 67b52af150 | |||
|
|
058a3fefaf | ||
| c8a97d940c | |||
|
|
00045c1e65 | ||
| d64d7dab58 | |||
|
|
4746a00eee | ||
| 3f8399d647 | |||
|
|
028b29fe08 | ||
| cb3f59a9d5 | |||
|
|
2b8d02d95c | ||
| d90d8cda11 | |||
|
|
17333df7af | ||
| 681d53e856 | |||
|
|
dc608fd257 | ||
| cd2efdc2f3 | |||
|
|
fd47867101 | ||
| 2ca1a6956c | |||
|
|
a5ad4f4c3c | ||
| 8b202e0c60 | |||
|
|
4626d0a1a7 | ||
| da72121109 | |||
|
|
5bebadf17c | ||
| 1bab0b14ef | |||
|
|
39f323d050 | ||
| 7cd9e14618 | |||
|
|
b77d2c16ab | ||
|
|
324f150950 | ||
| dd6bcf843c | |||
|
|
874cc4971b | ||
| bfe4695b2d | |||
|
|
99161c8e7d | ||
| 6901791dd2 | |||
|
|
acb3d14b47 | ||
| aa5f7e271e | |||
|
|
0a01995f53 | ||
| 97f71d125e | |||
|
|
8db2f3926c | ||
|
|
2b68b59eca |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ generate_protos.sh
|
|||||||
update_dep.sh
|
update_dep.sh
|
||||||
.vscode/
|
.vscode/
|
||||||
.gocache/
|
.gocache/
|
||||||
|
.cache/
|
||||||
@@ -31,7 +31,7 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.48.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
@@ -49,6 +49,6 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||||
google.golang.org/protobuf v1.36.10
|
google.golang.org/protobuf v1.36.11
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
@@ -212,12 +212,12 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -90,10 +90,6 @@ func (c *quoteCalculator) Compute(ctx context.Context, plan *model.FeePlan, inte
|
|||||||
}
|
}
|
||||||
|
|
||||||
ledgerAccountRef := strings.TrimSpace(rule.LedgerAccountRef)
|
ledgerAccountRef := strings.TrimSpace(rule.LedgerAccountRef)
|
||||||
if ledgerAccountRef == "" {
|
|
||||||
c.logger.Warn("fee rule missing ledger account reference", zap.String("rule_id", rule.RuleID))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
amount, scale, calcErr := c.calculateRuleAmount(baseAmount, baseScale, rule)
|
amount, scale, calcErr := c.calculateRuleAmount(baseAmount, baseScale, rule)
|
||||||
if calcErr != nil {
|
if calcErr != nil {
|
||||||
@@ -113,7 +109,8 @@ func (c *quoteCalculator) Compute(ctx context.Context, plan *model.FeePlan, inte
|
|||||||
|
|
||||||
entrySide := mapEntrySide(rule.EntrySide)
|
entrySide := mapEntrySide(rule.EntrySide)
|
||||||
if entrySide == accountingv1.EntrySide_ENTRY_SIDE_UNSPECIFIED {
|
if entrySide == accountingv1.EntrySide_ENTRY_SIDE_UNSPECIFIED {
|
||||||
entrySide = accountingv1.EntrySide_ENTRY_SIDE_CREDIT
|
// Default fees to debit (i.e. charge the customer) when entry side is not specified.
|
||||||
|
entrySide = accountingv1.EntrySide_ENTRY_SIDE_DEBIT
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := map[string]string{
|
meta := map[string]string{
|
||||||
|
|||||||
@@ -38,23 +38,23 @@ func New(plans storage.PlansStore, logger *zap.Logger) *feeResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *feeResolver) ResolveFeeRule(ctx context.Context, orgID *primitive.ObjectID, trigger model.Trigger, at time.Time, attrs map[string]string) (*model.FeePlan, *model.FeeRule, error) {
|
func (r *feeResolver) ResolveFeeRule(ctx context.Context, orgRef *primitive.ObjectID, trigger model.Trigger, at time.Time, attrs map[string]string) (*model.FeePlan, *model.FeeRule, error) {
|
||||||
if r.plans == nil {
|
if r.plans == nil {
|
||||||
return nil, nil, merrors.InvalidArgument("fees: plans store is required")
|
return nil, nil, merrors.InvalidArgument("fees: plans store is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try org-specific first if provided.
|
// Try org-specific first if provided.
|
||||||
if orgID != nil && !orgID.IsZero() {
|
if orgRef != nil && !orgRef.IsZero() {
|
||||||
if plan, err := r.getOrgPlan(ctx, *orgID, at); err == nil {
|
if plan, err := r.getOrgPlan(ctx, *orgRef, at); err == nil {
|
||||||
if rule, selErr := selectRule(plan, trigger, at, attrs); selErr == nil {
|
if rule, selErr := selectRule(plan, trigger, at, attrs); selErr == nil {
|
||||||
return plan, rule, nil
|
return plan, rule, nil
|
||||||
} else if !errors.Is(selErr, ErrNoFeeRuleFound) {
|
} else if !errors.Is(selErr, ErrNoFeeRuleFound) {
|
||||||
r.logger.Warn("failed selecting rule for org plan", zap.Error(selErr), zap.String("org_ref", orgID.Hex()))
|
r.logger.Warn("failed selecting rule for org plan", zap.Error(selErr), zap.String("org_ref", orgRef.Hex()))
|
||||||
return nil, nil, selErr
|
return nil, nil, selErr
|
||||||
}
|
}
|
||||||
r.logger.Debug("no matching rule in org plan; falling back to global", zap.String("org_ref", orgID.Hex()))
|
r.logger.Debug("no matching rule in org plan; falling back to global", zap.String("org_ref", orgRef.Hex()))
|
||||||
} else if !errors.Is(err, storage.ErrFeePlanNotFound) {
|
} else if !errors.Is(err, storage.ErrFeePlanNotFound) {
|
||||||
r.logger.Warn("failed resolving org fee plan", zap.Error(err), zap.String("org_ref", orgID.Hex()))
|
r.logger.Warn("failed resolving org fee plan", zap.Error(err), zap.String("org_ref", orgRef.Hex()))
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ func TestResolver_GlobalFallbackWhenOrgMissing(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected fallback to global, got error: %v", err)
|
t.Fatalf("expected fallback to global, got error: %v", err)
|
||||||
}
|
}
|
||||||
if !plan.GetOrganizationRef().IsZero() {
|
if plan.OrganizationRef != nil && !plan.OrganizationRef.IsZero() {
|
||||||
t.Fatalf("expected global plan, got orgRef %s", plan.GetOrganizationRef().Hex())
|
t.Fatalf("expected global plan, got orgRef %s", plan.OrganizationRef.Hex())
|
||||||
}
|
}
|
||||||
if rule.RuleID != "global_capture" {
|
if rule.RuleID != "global_capture" {
|
||||||
t.Fatalf("unexpected rule selected: %s", rule.RuleID)
|
t.Fatalf("unexpected rule selected: %s", rule.RuleID)
|
||||||
@@ -59,8 +59,7 @@ func TestResolver_OrgOverridesGlobal(t *testing.T) {
|
|||||||
{RuleID: "org_capture", Trigger: model.TriggerCapture, Priority: 10, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)},
|
{RuleID: "org_capture", Trigger: model.TriggerCapture, Priority: 10, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
orgPlan.SetOrganizationRef(org)
|
orgPlan.OrganizationRef = &org
|
||||||
|
|
||||||
store := &memoryPlansStore{plans: []*model.FeePlan{globalPlan, orgPlan}}
|
store := &memoryPlansStore{plans: []*model.FeePlan{globalPlan, orgPlan}}
|
||||||
resolver := New(store, zap.NewNop())
|
resolver := New(store, zap.NewNop())
|
||||||
|
|
||||||
@@ -95,7 +94,7 @@ func TestResolver_SelectsHighestPriority(t *testing.T) {
|
|||||||
{RuleID: "high", Trigger: model.TriggerCapture, Priority: 200, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)},
|
{RuleID: "high", Trigger: model.TriggerCapture, Priority: 200, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
plan.SetOrganizationRef(org)
|
plan.OrganizationRef = &org
|
||||||
|
|
||||||
store := &memoryPlansStore{plans: []*model.FeePlan{plan}}
|
store := &memoryPlansStore{plans: []*model.FeePlan{plan}}
|
||||||
resolver := New(store, zap.NewNop())
|
resolver := New(store, zap.NewNop())
|
||||||
@@ -136,7 +135,7 @@ func TestResolver_EffectiveDateFiltering(t *testing.T) {
|
|||||||
{RuleID: "expired", Trigger: model.TriggerCapture, Priority: 100, Percentage: "0.05", EffectiveFrom: past, EffectiveTo: &past},
|
{RuleID: "expired", Trigger: model.TriggerCapture, Priority: 100, Percentage: "0.05", EffectiveFrom: past, EffectiveTo: &past},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
orgPlan.SetOrganizationRef(org)
|
orgPlan.OrganizationRef = &org
|
||||||
|
|
||||||
globalPlan := &model.FeePlan{
|
globalPlan := &model.FeePlan{
|
||||||
Active: true,
|
Active: true,
|
||||||
@@ -221,7 +220,7 @@ func TestResolver_MultipleActivePlansConflict(t *testing.T) {
|
|||||||
{RuleID: "r1", Trigger: model.TriggerCapture, Priority: 10, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)},
|
{RuleID: "r1", Trigger: model.TriggerCapture, Priority: 10, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
p1.SetOrganizationRef(org)
|
p1.OrganizationRef = &org
|
||||||
p2 := &model.FeePlan{
|
p2 := &model.FeePlan{
|
||||||
Active: true,
|
Active: true,
|
||||||
EffectiveFrom: now.Add(-30 * time.Minute),
|
EffectiveFrom: now.Add(-30 * time.Minute),
|
||||||
@@ -229,7 +228,7 @@ func TestResolver_MultipleActivePlansConflict(t *testing.T) {
|
|||||||
{RuleID: "r2", Trigger: model.TriggerCapture, Priority: 20, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)},
|
{RuleID: "r2", Trigger: model.TriggerCapture, Priority: 20, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
p2.SetOrganizationRef(org)
|
p2.OrganizationRef = &org
|
||||||
|
|
||||||
store := &memoryPlansStore{plans: []*model.FeePlan{p1, p2}}
|
store := &memoryPlansStore{plans: []*model.FeePlan{p1, p2}}
|
||||||
resolver := New(store, zap.NewNop())
|
resolver := New(store, zap.NewNop())
|
||||||
@@ -263,7 +262,7 @@ func (m *memoryPlansStore) GetActivePlan(ctx context.Context, orgRef primitive.O
|
|||||||
func (m *memoryPlansStore) FindActiveOrgPlan(_ context.Context, orgRef primitive.ObjectID, at time.Time) (*model.FeePlan, error) {
|
func (m *memoryPlansStore) FindActiveOrgPlan(_ context.Context, orgRef primitive.ObjectID, at time.Time) (*model.FeePlan, error) {
|
||||||
var matches []*model.FeePlan
|
var matches []*model.FeePlan
|
||||||
for _, plan := range m.plans {
|
for _, plan := range m.plans {
|
||||||
if plan == nil || plan.GetOrganizationRef() != orgRef {
|
if plan == nil || plan.OrganizationRef == nil || plan.OrganizationRef.IsZero() || (*plan.OrganizationRef != orgRef) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !plan.Active {
|
if !plan.Active {
|
||||||
@@ -289,7 +288,7 @@ func (m *memoryPlansStore) FindActiveOrgPlan(_ context.Context, orgRef primitive
|
|||||||
func (m *memoryPlansStore) FindActiveGlobalPlan(_ context.Context, at time.Time) (*model.FeePlan, error) {
|
func (m *memoryPlansStore) FindActiveGlobalPlan(_ context.Context, at time.Time) (*model.FeePlan, error) {
|
||||||
var matches []*model.FeePlan
|
var matches []*model.FeePlan
|
||||||
for _, plan := range m.plans {
|
for _, plan := range m.plans {
|
||||||
if plan == nil || !plan.GetOrganizationRef().IsZero() {
|
if plan == nil || ((plan.OrganizationRef != nil) && !plan.OrganizationRef.IsZero()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !plan.Active {
|
if !plan.Active {
|
||||||
|
|||||||
88
api/billing/fees/internal/service/fees/logging.go
Normal file
88
api/billing/fees/internal/service/fees/logging.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package fees
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||||
|
tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func requestLogFields(meta *feesv1.RequestMeta, intent *feesv1.Intent) []zap.Field {
|
||||||
|
fields := logFieldsFromRequestMeta(meta)
|
||||||
|
fields = append(fields, logFieldsFromIntent(intent)...)
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func logFieldsFromRequestMeta(meta *feesv1.RequestMeta) []zap.Field {
|
||||||
|
if meta == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fields := make([]zap.Field, 0, 4)
|
||||||
|
if org := strings.TrimSpace(meta.GetOrganizationRef()); org != "" {
|
||||||
|
fields = append(fields, zap.String("organization_ref", org))
|
||||||
|
}
|
||||||
|
fields = append(fields, logFieldsFromTrace(meta.GetTrace())...)
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func logFieldsFromIntent(intent *feesv1.Intent) []zap.Field {
|
||||||
|
if intent == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fields := make([]zap.Field, 0, 5)
|
||||||
|
if trigger := intent.GetTrigger(); trigger != feesv1.Trigger_TRIGGER_UNSPECIFIED {
|
||||||
|
fields = append(fields, zap.String("trigger", trigger.String()))
|
||||||
|
}
|
||||||
|
if base := intent.GetBaseAmount(); base != nil {
|
||||||
|
if amount := strings.TrimSpace(base.GetAmount()); amount != "" {
|
||||||
|
fields = append(fields, zap.String("base_amount", amount))
|
||||||
|
}
|
||||||
|
if currency := strings.TrimSpace(base.GetCurrency()); currency != "" {
|
||||||
|
fields = append(fields, zap.String("base_currency", currency))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if booked := intent.GetBookedAt(); booked != nil && booked.IsValid() {
|
||||||
|
fields = append(fields, zap.Time("booked_at", booked.AsTime()))
|
||||||
|
}
|
||||||
|
if attrs := intent.GetAttributes(); len(attrs) > 0 {
|
||||||
|
fields = append(fields, zap.Int("attributes_count", len(attrs)))
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func logFieldsFromTrace(trace *tracev1.TraceContext) []zap.Field {
|
||||||
|
if trace == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fields := make([]zap.Field, 0, 3)
|
||||||
|
if reqRef := strings.TrimSpace(trace.GetRequestRef()); reqRef != "" {
|
||||||
|
fields = append(fields, zap.String("request_ref", reqRef))
|
||||||
|
}
|
||||||
|
if idem := strings.TrimSpace(trace.GetIdempotencyKey()); idem != "" {
|
||||||
|
fields = append(fields, zap.String("idempotency_key", idem))
|
||||||
|
}
|
||||||
|
if traceRef := strings.TrimSpace(trace.GetTraceRef()); traceRef != "" {
|
||||||
|
fields = append(fields, zap.String("trace_ref", traceRef))
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func logFieldsFromTokenPayload(payload *feeQuoteTokenPayload) []zap.Field {
|
||||||
|
if payload == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fields := make([]zap.Field, 0, 6)
|
||||||
|
if org := strings.TrimSpace(payload.OrganizationRef); org != "" {
|
||||||
|
fields = append(fields, zap.String("organization_ref", org))
|
||||||
|
}
|
||||||
|
if payload.ExpiresAtUnixMs > 0 {
|
||||||
|
fields = append(fields,
|
||||||
|
zap.Int64("expires_at_unix_ms", payload.ExpiresAtUnixMs),
|
||||||
|
zap.Time("expires_at", time.UnixMilli(payload.ExpiresAtUnixMs)))
|
||||||
|
}
|
||||||
|
fields = append(fields, logFieldsFromIntent(payload.Intent)...)
|
||||||
|
fields = append(fields, logFieldsFromTrace(payload.Trace)...)
|
||||||
|
return fields
|
||||||
|
}
|
||||||
@@ -72,26 +72,57 @@ func (s *Service) Register(router routers.GRPC) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (resp *feesv1.QuoteFeesResponse, err error) {
|
func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (resp *feesv1.QuoteFeesResponse, err error) {
|
||||||
|
var (
|
||||||
|
meta *feesv1.RequestMeta
|
||||||
|
intent *feesv1.Intent
|
||||||
|
)
|
||||||
|
if req != nil {
|
||||||
|
meta = req.GetMeta()
|
||||||
|
intent = req.GetIntent()
|
||||||
|
}
|
||||||
|
logger := s.logger.With(requestLogFields(meta, intent)...)
|
||||||
|
|
||||||
start := s.clock.Now()
|
start := s.clock.Now()
|
||||||
trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
|
trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
|
||||||
if req != nil && req.GetIntent() != nil {
|
if intent != nil {
|
||||||
trigger = req.GetIntent().GetTrigger()
|
trigger = intent.GetTrigger()
|
||||||
}
|
}
|
||||||
var fxUsed bool
|
var fxUsed bool
|
||||||
defer func() {
|
defer func() {
|
||||||
statusLabel := statusFromError(err)
|
statusLabel := statusFromError(err)
|
||||||
|
linesCount := 0
|
||||||
|
appliedCount := 0
|
||||||
if err == nil && resp != nil {
|
if err == nil && resp != nil {
|
||||||
fxUsed = resp.GetFxUsed() != nil
|
fxUsed = resp.GetFxUsed() != nil
|
||||||
|
linesCount = len(resp.GetLines())
|
||||||
|
appliedCount = len(resp.GetApplied())
|
||||||
}
|
}
|
||||||
observeMetrics("quote", trigger, statusLabel, fxUsed, time.Since(start))
|
observeMetrics("quote", trigger, statusLabel, fxUsed, time.Since(start))
|
||||||
|
|
||||||
|
logFields := []zap.Field{
|
||||||
|
zap.String("status", statusLabel),
|
||||||
|
zap.Duration("duration", time.Since(start)),
|
||||||
|
zap.Bool("fx_used", fxUsed),
|
||||||
|
zap.String("trigger", trigger.String()),
|
||||||
|
zap.Int("lines", linesCount),
|
||||||
|
zap.Int("applied_rules", appliedCount),
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("QuoteFees finished", append(logFields, zap.Error(err))...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("QuoteFees finished", logFields...)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
logger.Debug("QuoteFees request received")
|
||||||
|
|
||||||
if err = s.validateQuoteRequest(req); err != nil {
|
if err = s.validateQuoteRequest(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
orgRef, parseErr := primitive.ObjectIDFromHex(req.GetMeta().GetOrganizationRef())
|
orgRef, parseErr := primitive.ObjectIDFromHex(req.GetMeta().GetOrganizationRef())
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
|
logger.Warn("QuoteFees invalid organization_ref", zap.Error(parseErr))
|
||||||
err = status.Error(codes.InvalidArgument, "invalid organization_ref")
|
err = status.Error(codes.InvalidArgument, "invalid organization_ref")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -112,20 +143,59 @@ func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFeesRequest) (resp *feesv1.PrecomputeFeesResponse, err error) {
|
func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFeesRequest) (resp *feesv1.PrecomputeFeesResponse, err error) {
|
||||||
|
var (
|
||||||
|
meta *feesv1.RequestMeta
|
||||||
|
intent *feesv1.Intent
|
||||||
|
)
|
||||||
|
if req != nil {
|
||||||
|
meta = req.GetMeta()
|
||||||
|
intent = req.GetIntent()
|
||||||
|
}
|
||||||
|
logger := s.logger.With(requestLogFields(meta, intent)...)
|
||||||
|
|
||||||
start := s.clock.Now()
|
start := s.clock.Now()
|
||||||
trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
|
trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
|
||||||
if req != nil && req.GetIntent() != nil {
|
if intent != nil {
|
||||||
trigger = req.GetIntent().GetTrigger()
|
trigger = intent.GetTrigger()
|
||||||
}
|
}
|
||||||
var fxUsed bool
|
var (
|
||||||
|
fxUsed bool
|
||||||
|
expiresAt time.Time
|
||||||
|
)
|
||||||
defer func() {
|
defer func() {
|
||||||
statusLabel := statusFromError(err)
|
statusLabel := statusFromError(err)
|
||||||
|
linesCount := 0
|
||||||
|
appliedCount := 0
|
||||||
if err == nil && resp != nil {
|
if err == nil && resp != nil {
|
||||||
fxUsed = resp.GetFxUsed() != nil
|
fxUsed = resp.GetFxUsed() != nil
|
||||||
|
linesCount = len(resp.GetLines())
|
||||||
|
appliedCount = len(resp.GetApplied())
|
||||||
|
if ts := resp.GetExpiresAt(); ts != nil {
|
||||||
|
expiresAt = ts.AsTime()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
observeMetrics("precompute", trigger, statusLabel, fxUsed, time.Since(start))
|
observeMetrics("precompute", trigger, statusLabel, fxUsed, time.Since(start))
|
||||||
|
|
||||||
|
logFields := []zap.Field{
|
||||||
|
zap.String("status", statusLabel),
|
||||||
|
zap.Duration("duration", time.Since(start)),
|
||||||
|
zap.Bool("fx_used", fxUsed),
|
||||||
|
zap.String("trigger", trigger.String()),
|
||||||
|
zap.Int("lines", linesCount),
|
||||||
|
zap.Int("applied_rules", appliedCount),
|
||||||
|
}
|
||||||
|
if !expiresAt.IsZero() {
|
||||||
|
logFields = append(logFields, zap.Time("expires_at", expiresAt))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("PrecomputeFees finished", append(logFields, zap.Error(err))...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("PrecomputeFees finished", logFields...)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
logger.Debug("PrecomputeFees request received")
|
||||||
|
|
||||||
if err = s.validatePrecomputeRequest(req); err != nil {
|
if err = s.validatePrecomputeRequest(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -134,6 +204,7 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
|
|||||||
|
|
||||||
orgRef, parseErr := primitive.ObjectIDFromHex(req.GetMeta().GetOrganizationRef())
|
orgRef, parseErr := primitive.ObjectIDFromHex(req.GetMeta().GetOrganizationRef())
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
|
logger.Warn("PrecomputeFees invalid organization_ref", zap.Error(parseErr))
|
||||||
err = status.Error(codes.InvalidArgument, "invalid organization_ref")
|
err = status.Error(codes.InvalidArgument, "invalid organization_ref")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -148,7 +219,7 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
|
|||||||
if ttl <= 0 {
|
if ttl <= 0 {
|
||||||
ttl = 60000
|
ttl = 60000
|
||||||
}
|
}
|
||||||
expiresAt := now.Add(time.Duration(ttl) * time.Millisecond)
|
expiresAt = now.Add(time.Duration(ttl) * time.Millisecond)
|
||||||
|
|
||||||
payload := feeQuoteTokenPayload{
|
payload := feeQuoteTokenPayload{
|
||||||
OrganizationRef: req.GetMeta().GetOrganizationRef(),
|
OrganizationRef: req.GetMeta().GetOrganizationRef(),
|
||||||
@@ -159,7 +230,7 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
|
|||||||
|
|
||||||
var token string
|
var token string
|
||||||
if token, err = encodeTokenPayload(payload); err != nil {
|
if token, err = encodeTokenPayload(payload); err != nil {
|
||||||
s.logger.Warn("failed to encode fee quote token", zap.Error(err))
|
logger.Warn("failed to encode fee quote token", zap.Error(err))
|
||||||
err = status.Error(codes.Internal, "failed to encode fee quote token")
|
err = status.Error(codes.Internal, "failed to encode fee quote token")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -176,9 +247,18 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeTokenRequest) (resp *feesv1.ValidateFeeTokenResponse, err error) {
|
func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeTokenRequest) (resp *feesv1.ValidateFeeTokenResponse, err error) {
|
||||||
|
tokenLen := 0
|
||||||
|
if req != nil {
|
||||||
|
tokenLen = len(strings.TrimSpace(req.GetFeeQuoteToken()))
|
||||||
|
}
|
||||||
|
logger := s.logger.With(zap.Int("token_length", tokenLen))
|
||||||
|
|
||||||
start := s.clock.Now()
|
start := s.clock.Now()
|
||||||
trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
|
trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
|
||||||
var fxUsed bool
|
var (
|
||||||
|
fxUsed bool
|
||||||
|
resultReason string
|
||||||
|
)
|
||||||
defer func() {
|
defer func() {
|
||||||
statusLabel := statusFromError(err)
|
statusLabel := statusFromError(err)
|
||||||
if err == nil && resp != nil {
|
if err == nil && resp != nil {
|
||||||
@@ -191,9 +271,28 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
observeMetrics("validate", trigger, statusLabel, fxUsed, time.Since(start))
|
observeMetrics("validate", trigger, statusLabel, fxUsed, time.Since(start))
|
||||||
|
|
||||||
|
logFields := []zap.Field{
|
||||||
|
zap.String("status", statusLabel),
|
||||||
|
zap.Duration("duration", time.Since(start)),
|
||||||
|
zap.Bool("fx_used", fxUsed),
|
||||||
|
zap.String("trigger", trigger.String()),
|
||||||
|
zap.Bool("valid", resp != nil && resp.GetValid()),
|
||||||
|
}
|
||||||
|
if resultReason != "" {
|
||||||
|
logFields = append(logFields, zap.String("reason", resultReason))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("ValidateFeeToken finished", append(logFields, zap.Error(err))...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("ValidateFeeToken finished", logFields...)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
logger.Debug("ValidateFeeToken request received")
|
||||||
|
|
||||||
if req == nil || strings.TrimSpace(req.GetFeeQuoteToken()) == "" {
|
if req == nil || strings.TrimSpace(req.GetFeeQuoteToken()) == "" {
|
||||||
|
resultReason = "missing_token"
|
||||||
err = status.Error(codes.InvalidArgument, "fee_quote_token is required")
|
err = status.Error(codes.InvalidArgument, "fee_quote_token is required")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -202,21 +301,29 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
|
|||||||
|
|
||||||
payload, decodeErr := decodeTokenPayload(req.GetFeeQuoteToken())
|
payload, decodeErr := decodeTokenPayload(req.GetFeeQuoteToken())
|
||||||
if decodeErr != nil {
|
if decodeErr != nil {
|
||||||
s.logger.Warn("failed to decode fee quote token", zap.Error(decodeErr))
|
resultReason = "invalid_token"
|
||||||
|
logger.Warn("failed to decode fee quote token", zap.Error(decodeErr))
|
||||||
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"}
|
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger = logger.With(logFieldsFromTokenPayload(&payload)...)
|
||||||
|
|
||||||
|
if payload.Intent != nil {
|
||||||
trigger = payload.Intent.GetTrigger()
|
trigger = payload.Intent.GetTrigger()
|
||||||
|
}
|
||||||
|
|
||||||
if now.UnixMilli() > payload.ExpiresAtUnixMs {
|
if now.UnixMilli() > payload.ExpiresAtUnixMs {
|
||||||
|
resultReason = "expired"
|
||||||
|
logger.Info("fee quote token expired")
|
||||||
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "expired"}
|
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "expired"}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
orgRef, parseErr := primitive.ObjectIDFromHex(payload.OrganizationRef)
|
orgRef, parseErr := primitive.ObjectIDFromHex(payload.OrganizationRef)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
s.logger.Warn("token contained invalid organization reference", zap.Error(parseErr))
|
resultReason = "invalid_token"
|
||||||
|
logger.Warn("token contained invalid organization reference", zap.Error(parseErr))
|
||||||
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"}
|
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -280,6 +387,16 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef primitive.Obj
|
|||||||
bookedAt = intent.GetBookedAt().AsTime()
|
bookedAt = intent.GetBookedAt().AsTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logFields := []zap.Field{
|
||||||
|
zap.Time("booked_at_used", bookedAt),
|
||||||
|
}
|
||||||
|
if !orgRef.IsZero() {
|
||||||
|
logFields = append(logFields, zap.String("organization_ref", orgRef.Hex()))
|
||||||
|
}
|
||||||
|
logFields = append(logFields, logFieldsFromIntent(intent)...)
|
||||||
|
logFields = append(logFields, logFieldsFromTrace(trace)...)
|
||||||
|
logger := s.logger.With(logFields...)
|
||||||
|
|
||||||
var orgPtr *primitive.ObjectID
|
var orgPtr *primitive.ObjectID
|
||||||
if !orgRef.IsZero() {
|
if !orgRef.IsZero() {
|
||||||
orgPtr = &orgRef
|
orgPtr = &orgRef
|
||||||
@@ -297,7 +414,7 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef primitive.Obj
|
|||||||
case errors.Is(err, storage.ErrFeePlanNotFound):
|
case errors.Is(err, storage.ErrFeePlanNotFound):
|
||||||
return nil, nil, nil, status.Error(codes.NotFound, "fee plan not found")
|
return nil, nil, nil, status.Error(codes.NotFound, "fee plan not found")
|
||||||
default:
|
default:
|
||||||
s.logger.Warn("failed to resolve fee rule", zap.Error(err))
|
logger.Warn("failed to resolve fee rule", zap.Error(err))
|
||||||
return nil, nil, nil, status.Error(codes.Internal, "failed to resolve fee rule")
|
return nil, nil, nil, status.Error(codes.Internal, "failed to resolve fee rule")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,7 +430,7 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef primitive.Obj
|
|||||||
if errors.Is(calcErr, merrors.ErrInvalidArg) {
|
if errors.Is(calcErr, merrors.ErrInvalidArg) {
|
||||||
return nil, nil, nil, status.Error(codes.InvalidArgument, calcErr.Error())
|
return nil, nil, nil, status.Error(codes.InvalidArgument, calcErr.Error())
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to compute fee quote", zap.Error(calcErr))
|
logger.Warn("failed to compute fee quote", zap.Error(calcErr))
|
||||||
return nil, nil, nil, status.Error(codes.Internal, "failed to compute fee quote")
|
return nil, nil, nil, status.Error(codes.Internal, "failed to compute fee quote")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func TestQuoteFees_ComputesDerivedLines(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
plan.SetID(primitive.NewObjectID())
|
plan.SetID(primitive.NewObjectID())
|
||||||
plan.SetOrganizationRef(orgRef)
|
plan.OrganizationRef = &orgRef
|
||||||
|
|
||||||
service := NewService(
|
service := NewService(
|
||||||
zap.NewNop(),
|
zap.NewNop(),
|
||||||
@@ -163,7 +163,7 @@ func TestQuoteFees_FiltersByAttributesAndDates(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
plan.SetID(primitive.NewObjectID())
|
plan.SetID(primitive.NewObjectID())
|
||||||
plan.SetOrganizationRef(orgRef)
|
plan.OrganizationRef = &orgRef
|
||||||
|
|
||||||
service := NewService(
|
service := NewService(
|
||||||
zap.NewNop(),
|
zap.NewNop(),
|
||||||
@@ -224,7 +224,7 @@ func TestQuoteFees_RoundingDown(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
plan.SetID(primitive.NewObjectID())
|
plan.SetID(primitive.NewObjectID())
|
||||||
plan.SetOrganizationRef(orgRef)
|
plan.OrganizationRef = &orgRef
|
||||||
|
|
||||||
service := NewService(
|
service := NewService(
|
||||||
zap.NewNop(),
|
zap.NewNop(),
|
||||||
@@ -277,7 +277,7 @@ func TestQuoteFees_UsesInjectedCalculator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
plan.SetID(primitive.NewObjectID())
|
plan.SetID(primitive.NewObjectID())
|
||||||
plan.SetOrganizationRef(orgRef)
|
plan.OrganizationRef = &orgRef
|
||||||
|
|
||||||
result := &types.CalculationResult{
|
result := &types.CalculationResult{
|
||||||
Lines: []*feesv1.DerivedPostingLine{
|
Lines: []*feesv1.DerivedPostingLine{
|
||||||
@@ -353,7 +353,7 @@ func TestQuoteFees_PopulatesFxUsed(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
plan.SetID(primitive.NewObjectID())
|
plan.SetID(primitive.NewObjectID())
|
||||||
plan.SetOrganizationRef(orgRef)
|
plan.OrganizationRef = &orgRef
|
||||||
|
|
||||||
fakeOracle := &oracleclient.Fake{
|
fakeOracle := &oracleclient.Fake{
|
||||||
LatestRateFn: func(ctx context.Context, req oracleclient.LatestRateParams) (*oracleclient.RateSnapshot, error) {
|
LatestRateFn: func(ctx context.Context, req oracleclient.LatestRateParams) (*oracleclient.RateSnapshot, error) {
|
||||||
@@ -452,7 +452,7 @@ func (s *stubPlansStore) FindActiveOrgPlan(_ context.Context, orgRef primitive.O
|
|||||||
if s.plan == nil {
|
if s.plan == nil {
|
||||||
return nil, storage.ErrFeePlanNotFound
|
return nil, storage.ErrFeePlanNotFound
|
||||||
}
|
}
|
||||||
if s.plan.GetOrganizationRef() != orgRef {
|
if (s.plan.OrganizationRef != nil) && (*s.plan.OrganizationRef != orgRef) {
|
||||||
return nil, storage.ErrFeePlanNotFound
|
return nil, storage.ErrFeePlanNotFound
|
||||||
}
|
}
|
||||||
if !s.plan.Active {
|
if !s.plan.Active {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/pkg/db/storable"
|
"github.com/tech/sendico/pkg/db/storable"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -26,8 +27,8 @@ const (
|
|||||||
// FeePlan describes a collection of fee rules for an organisation.
|
// FeePlan describes a collection of fee rules for an organisation.
|
||||||
type FeePlan struct {
|
type FeePlan struct {
|
||||||
storable.Base `bson:",inline" json:",inline"`
|
storable.Base `bson:",inline" json:",inline"`
|
||||||
model.OrganizationBoundBase `bson:",inline" json:",inline"`
|
|
||||||
model.Describable `bson:",inline" json:",inline"`
|
model.Describable `bson:",inline" json:",inline"`
|
||||||
|
OrganizationRef *primitive.ObjectID `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"`
|
||||||
Active bool `bson:"active" json:"active"`
|
Active bool `bson:"active" json:"active"`
|
||||||
EffectiveFrom time.Time `bson:"effectiveFrom" json:"effectiveFrom"`
|
EffectiveFrom time.Time `bson:"effectiveFrom" json:"effectiveFrom"`
|
||||||
EffectiveTo *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"`
|
EffectiveTo *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"`
|
||||||
|
|||||||
@@ -11,12 +11,18 @@ market:
|
|||||||
- driver: CBR
|
- driver: CBR
|
||||||
settings:
|
settings:
|
||||||
base_url: "https://www.cbr.ru"
|
base_url: "https://www.cbr.ru"
|
||||||
|
user_agent: "Mozilla/5.0 (compatible; SendicoFX/1.0; +https://app.sendico.io)"
|
||||||
|
accept_header: "application/xml,text/xml;q=0.9,*/*;q=0.8"
|
||||||
pairs:
|
pairs:
|
||||||
BINANCE:
|
BINANCE:
|
||||||
- base: "USDT"
|
- base: "USDT"
|
||||||
quote: "EUR"
|
quote: "EUR"
|
||||||
symbol: "EURUSDT"
|
symbol: "EURUSDT"
|
||||||
invert: true
|
invert: true
|
||||||
|
- base: "USD"
|
||||||
|
quote: "USDT"
|
||||||
|
symbol: "USDTUSD"
|
||||||
|
invert: true
|
||||||
- base: "UAH"
|
- base: "UAH"
|
||||||
quote: "USDT"
|
quote: "USDT"
|
||||||
symbol: "USDTUAH"
|
symbol: "USDTUAH"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.48.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
@@ -49,7 +49,7 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||||
google.golang.org/grpc v1.77.0 // indirect
|
google.golang.org/grpc v1.77.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
@@ -212,12 +212,12 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ func (s *Service) executePoll(ctx context.Context) error {
|
|||||||
|
|
||||||
func (s *Service) pollOnce(ctx context.Context) error {
|
func (s *Service) pollOnce(ctx context.Context) error {
|
||||||
var firstErr error
|
var firstErr error
|
||||||
|
failures := 0
|
||||||
for _, pair := range s.pairs {
|
for _, pair := range s.pairs {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
err := s.upsertPair(ctx, pair)
|
err := s.upsertPair(ctx, pair)
|
||||||
@@ -96,14 +97,24 @@ func (s *Service) pollOnce(ctx context.Context) error {
|
|||||||
if firstErr == nil {
|
if firstErr == nil {
|
||||||
firstErr = err
|
firstErr = err
|
||||||
}
|
}
|
||||||
|
failures++
|
||||||
s.logger.Warn("Failed to ingest pair",
|
s.logger.Warn("Failed to ingest pair",
|
||||||
zap.String("symbol", pair.Symbol),
|
zap.String("symbol", pair.Symbol),
|
||||||
zap.String("source", pair.Source.String()),
|
zap.String("source", pair.Source.String()),
|
||||||
|
zap.String("provider", pair.Provider),
|
||||||
|
zap.String("base", pair.Base),
|
||||||
|
zap.String("quote", pair.Quote),
|
||||||
|
zap.Bool("invert", pair.Invert),
|
||||||
zap.Duration("elapsed", elapsed),
|
zap.Duration("elapsed", elapsed),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if failures > 0 {
|
||||||
|
s.logger.Warn("Ingestion poll completed with failures", zap.Int("failures", failures), zap.Int("total", len(s.pairs)))
|
||||||
|
} else {
|
||||||
|
s.logger.Info("Ingestion poll completed", zap.Int("total", len(s.pairs)))
|
||||||
|
}
|
||||||
return firstErr
|
return firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +126,7 @@ func (s *Service) upsertPair(ctx context.Context, pair config.Pair) error {
|
|||||||
|
|
||||||
ticker, err := connector.FetchTicker(ctx, pair.Symbol)
|
ticker, err := connector.FetchTicker(ctx, pair.Symbol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return merrors.InternalWrap(err, "fetch ticker")
|
return merrors.InternalWrap(err, "fetch ticker: "+pair.Symbol)
|
||||||
}
|
}
|
||||||
|
|
||||||
bid, err := parseDecimal(ticker.BidPrice)
|
bid, err := parseDecimal(ticker.BidPrice)
|
||||||
@@ -172,9 +183,14 @@ func (s *Service) upsertPair(ctx context.Context, pair config.Pair) error {
|
|||||||
s.logger.Debug("Snapshot ingested",
|
s.logger.Debug("Snapshot ingested",
|
||||||
zap.String("pair", pair.Base+"/"+pair.Quote),
|
zap.String("pair", pair.Base+"/"+pair.Quote),
|
||||||
zap.String("provider", pair.Provider),
|
zap.String("provider", pair.Provider),
|
||||||
|
zap.String("source", pair.Source.String()),
|
||||||
|
zap.String("provider_ref", snapshot.ProviderRef),
|
||||||
zap.String("bid", snapshot.Bid),
|
zap.String("bid", snapshot.Bid),
|
||||||
zap.String("ask", snapshot.Ask),
|
zap.String("ask", snapshot.Ask),
|
||||||
zap.String("mid", snapshot.Mid),
|
zap.String("mid", snapshot.Mid),
|
||||||
|
zap.String("spread_bps", snapshot.SpreadBps),
|
||||||
|
zap.Int64("asof_unix_ms", snapshot.AsOfUnixMs),
|
||||||
|
zap.String("rate_ref", snapshot.RateRef),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
type cbrConnector struct {
|
type cbrConnector struct {
|
||||||
id mmodel.Driver
|
id mmodel.Driver
|
||||||
provider string
|
provider string
|
||||||
client *http.Client
|
http *httpClient
|
||||||
base string
|
base string
|
||||||
dailyPath string
|
dailyPath string
|
||||||
directoryPath string
|
directoryPath string
|
||||||
@@ -60,6 +60,8 @@ func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Conne
|
|||||||
directoryPath := defaultDirectoryPath
|
directoryPath := defaultDirectoryPath
|
||||||
dailyPath := defaultDailyPath
|
dailyPath := defaultDailyPath
|
||||||
dynamicPath := defaultDynamicPath
|
dynamicPath := defaultDynamicPath
|
||||||
|
userAgent := defaultUserAgent
|
||||||
|
acceptHeader := defaultAccept
|
||||||
|
|
||||||
if settings != nil {
|
if settings != nil {
|
||||||
if value, ok := settings["base_url"].(string); ok && strings.TrimSpace(value) != "" {
|
if value, ok := settings["base_url"].(string); ok && strings.TrimSpace(value) != "" {
|
||||||
@@ -77,6 +79,12 @@ func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Conne
|
|||||||
if value, ok := settings["dynamic_path"].(string); ok && strings.TrimSpace(value) != "" {
|
if value, ok := settings["dynamic_path"].(string); ok && strings.TrimSpace(value) != "" {
|
||||||
dynamicPath = strings.TrimSpace(value)
|
dynamicPath = strings.TrimSpace(value)
|
||||||
}
|
}
|
||||||
|
if value, ok := settings["user_agent"].(string); ok && strings.TrimSpace(value) != "" {
|
||||||
|
userAgent = strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
if value, ok := settings["accept_header"].(string); ok && strings.TrimSpace(value) != "" {
|
||||||
|
acceptHeader = strings.TrimSpace(value)
|
||||||
|
}
|
||||||
dialTimeout = common.DurationSetting(settings, "dial_timeout_seconds", dialTimeout)
|
dialTimeout = common.DurationSetting(settings, "dial_timeout_seconds", dialTimeout)
|
||||||
dialKeepAlive = common.DurationSetting(settings, "dial_keep_alive_seconds", dialKeepAlive)
|
dialKeepAlive = common.DurationSetting(settings, "dial_keep_alive_seconds", dialKeepAlive)
|
||||||
tlsHandshakeTimeout = common.DurationSetting(settings, "tls_handshake_timeout_seconds", tlsHandshakeTimeout)
|
tlsHandshakeTimeout = common.DurationSetting(settings, "tls_handshake_timeout_seconds", tlsHandshakeTimeout)
|
||||||
@@ -99,13 +107,24 @@ func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Conne
|
|||||||
transport = customTransport
|
transport = customTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: requestTimeout,
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
referer := parsed.String()
|
||||||
|
|
||||||
connector := &cbrConnector{
|
connector := &cbrConnector{
|
||||||
id: mmodel.DriverCBR,
|
id: mmodel.DriverCBR,
|
||||||
provider: provider,
|
provider: provider,
|
||||||
client: &http.Client{
|
http: newHTTPClient(
|
||||||
Timeout: requestTimeout,
|
logger,
|
||||||
Transport: transport,
|
client,
|
||||||
|
httpClientOptions{
|
||||||
|
userAgent: userAgent,
|
||||||
|
accept: acceptHeader,
|
||||||
|
referer: referer,
|
||||||
},
|
},
|
||||||
|
),
|
||||||
base: strings.TrimRight(parsed.String(), "/"),
|
base: strings.TrimRight(parsed.String(), "/"),
|
||||||
dailyPath: dailyPath,
|
dailyPath: dailyPath,
|
||||||
directoryPath: directoryPath,
|
directoryPath: directoryPath,
|
||||||
@@ -161,20 +180,32 @@ func (c *cbrConnector) refreshDirectory() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
req, err := c.http.NewRequest(context.Background(), http.MethodGet, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return merrors.InternalWrap(err, "cbr: build directory request")
|
return merrors.InternalWrap(err, "cbr: build directory request")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.client.Do(req)
|
resp, err := c.http.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Warn("CBR directory request failed", zap.Error(err))
|
c.logger.Warn(
|
||||||
|
"CBR directory request failed",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("endpoint", endpoint),
|
||||||
|
zap.String("referer", c.http.headerValue("Referer")),
|
||||||
|
zap.String("user_agent", c.http.headerValue("User-Agent")),
|
||||||
|
)
|
||||||
return merrors.InternalWrap(err, "cbr: directory request failed")
|
return merrors.InternalWrap(err, "cbr: directory request failed")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
c.logger.Warn("CBR directory returned non-OK status", zap.Int("status", resp.StatusCode))
|
c.logger.Warn(
|
||||||
|
"CBR directory returned non-OK status",
|
||||||
|
zap.Int("status", resp.StatusCode),
|
||||||
|
zap.String("endpoint", endpoint),
|
||||||
|
zap.String("referer", c.http.headerValue("Referer")),
|
||||||
|
zap.String("user_agent", c.http.headerValue("User-Agent")),
|
||||||
|
)
|
||||||
return merrors.Internal("cbr: unexpected status " + strconv.Itoa(resp.StatusCode))
|
return merrors.Internal("cbr: unexpected status " + strconv.Itoa(resp.StatusCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,12 +214,13 @@ func (c *cbrConnector) refreshDirectory() error {
|
|||||||
|
|
||||||
var directory valuteDirectory
|
var directory valuteDirectory
|
||||||
if err := decoder.Decode(&directory); err != nil {
|
if err := decoder.Decode(&directory); err != nil {
|
||||||
c.logger.Warn("CBR directory decode failed", zap.Error(err))
|
c.logger.Warn("CBR directory decode failed", zap.Error(err), zap.String("endpoint", endpoint))
|
||||||
return merrors.InternalWrap(err, "cbr: decode directory")
|
return merrors.InternalWrap(err, "cbr: decode directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
mapping, err := buildValuteMapping(directory.Items)
|
mapping, err := buildValuteMapping(c.logger.Named("mapper"), directory.Items)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
c.logger.Warn("Failed to build currencies mapping", zap.Error(err), zap.String("endpoint", endpoint))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,23 +232,32 @@ func (c *cbrConnector) refreshDirectory() error {
|
|||||||
func (c *cbrConnector) fetchDailyRate(ctx context.Context, valute valuteInfo) (string, error) {
|
func (c *cbrConnector) fetchDailyRate(ctx context.Context, valute valuteInfo) (string, error) {
|
||||||
endpoint, err := c.buildURL(c.dailyPath, nil)
|
endpoint, err := c.buildURL(c.dailyPath, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
c.logger.Warn("Failed to build daily fetch URL", zap.Error(err), zap.String("path", c.dailyPath))
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
req, err := c.http.NewRequest(ctx, http.MethodGet, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
c.logger.Warn("Failed to request daily rate", zap.Error(err), zap.String("endpoint", endpoint))
|
||||||
return "", merrors.InternalWrap(err, "cbr: build daily request")
|
return "", merrors.InternalWrap(err, "cbr: build daily request")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.client.Do(req)
|
resp, err := c.http.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Warn("CBR daily request failed", zap.String("currency", valute.ISOCharCode), zap.Error(err))
|
c.logger.Warn("CBR daily request failed",
|
||||||
|
zap.Error(err), zap.String("currency", valute.ISOCharCode),
|
||||||
|
zap.String("endpoint", endpoint), zap.String("referer", c.http.headerValue("Referer")),
|
||||||
|
zap.String("user_agent", c.http.headerValue("User-Agent")),
|
||||||
|
)
|
||||||
return "", merrors.InternalWrap(err, "cbr: daily request failed")
|
return "", merrors.InternalWrap(err, "cbr: daily request failed")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
c.logger.Warn("CBR daily returned non-OK status", zap.Int("status", resp.StatusCode))
|
c.logger.Warn(
|
||||||
|
"CBR daily returned non-OK status", zap.Int("status", resp.StatusCode),
|
||||||
|
zap.String("currency", valute.ISOCharCode), zap.String("endpoint", endpoint),
|
||||||
|
)
|
||||||
return "", merrors.Internal("cbr: unexpected status " + strconv.Itoa(resp.StatusCode))
|
return "", merrors.Internal("cbr: unexpected status " + strconv.Itoa(resp.StatusCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +266,9 @@ func (c *cbrConnector) fetchDailyRate(ctx context.Context, valute valuteInfo) (s
|
|||||||
|
|
||||||
var payload dailyRates
|
var payload dailyRates
|
||||||
if err := decoder.Decode(&payload); err != nil {
|
if err := decoder.Decode(&payload); err != nil {
|
||||||
c.logger.Warn("CBR daily decode failed", zap.Error(err))
|
c.logger.Warn("CBR daily decode failed", zap.Error(err),
|
||||||
|
zap.String("currency", valute.ISOCharCode), zap.String("endpoint", endpoint),
|
||||||
|
)
|
||||||
return "", merrors.InternalWrap(err, "cbr: decode daily response")
|
return "", merrors.InternalWrap(err, "cbr: decode daily response")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,25 +290,40 @@ func (c *cbrConnector) fetchHistoricalRate(ctx context.Context, valute valuteInf
|
|||||||
"date_req2": date.Format("02/01/2006"),
|
"date_req2": date.Format("02/01/2006"),
|
||||||
"VAL_NM_RQ": valute.ID,
|
"VAL_NM_RQ": valute.ID,
|
||||||
}
|
}
|
||||||
|
dateStr := date.Format("2006-01-02")
|
||||||
endpoint, err := c.buildURL(c.dynamicPath, query)
|
endpoint, err := c.buildURL(c.dynamicPath, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
req, err := c.http.NewRequest(ctx, http.MethodGet, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", merrors.InternalWrap(err, "cbr: build historical request")
|
return "", merrors.InternalWrap(err, "cbr: build historical request")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.client.Do(req)
|
resp, err := c.http.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Warn("CBR historical request failed", zap.String("currency", valute.ISOCharCode), zap.Error(err))
|
c.logger.Warn(
|
||||||
|
"CBR historical request failed",
|
||||||
|
zap.String("currency", valute.ISOCharCode),
|
||||||
|
zap.String("date", dateStr),
|
||||||
|
zap.String("endpoint", endpoint),
|
||||||
|
zap.String("referer", c.http.headerValue("Referer")),
|
||||||
|
zap.String("user_agent", c.http.headerValue("User-Agent")),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return "", merrors.InternalWrap(err, "cbr: historical request failed")
|
return "", merrors.InternalWrap(err, "cbr: historical request failed")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
c.logger.Warn("CBR historical returned non-OK status", zap.Int("status", resp.StatusCode))
|
c.logger.Warn(
|
||||||
|
"CBR historical returned non-OK status",
|
||||||
|
zap.Int("status", resp.StatusCode),
|
||||||
|
zap.String("currency", valute.ISOCharCode),
|
||||||
|
zap.String("date", dateStr),
|
||||||
|
zap.String("endpoint", endpoint),
|
||||||
|
)
|
||||||
return "", merrors.Internal("cbr: unexpected status " + strconv.Itoa(resp.StatusCode))
|
return "", merrors.Internal("cbr: unexpected status " + strconv.Itoa(resp.StatusCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +332,13 @@ func (c *cbrConnector) fetchHistoricalRate(ctx context.Context, valute valuteInf
|
|||||||
|
|
||||||
var payload dynamicRates
|
var payload dynamicRates
|
||||||
if err := decoder.Decode(&payload); err != nil {
|
if err := decoder.Decode(&payload); err != nil {
|
||||||
c.logger.Warn("CBR historical decode failed", zap.Error(err))
|
c.logger.Warn(
|
||||||
|
"CBR historical decode failed",
|
||||||
|
zap.String("currency", valute.ISOCharCode),
|
||||||
|
zap.String("date", dateStr),
|
||||||
|
zap.String("endpoint", endpoint),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return "", merrors.InternalWrap(err, "cbr: decode historical response")
|
return "", merrors.InternalWrap(err, "cbr: decode historical response")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +401,7 @@ type valuteMapping struct {
|
|||||||
byID map[string]valuteInfo
|
byID map[string]valuteInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildValuteMapping(items []valuteItem) (*valuteMapping, error) {
|
func buildValuteMapping(logger *zap.Logger, items []valuteItem) (*valuteMapping, error) {
|
||||||
byISO := make(map[string]valuteInfo, len(items))
|
byISO := make(map[string]valuteInfo, len(items))
|
||||||
byID := make(map[string]valuteInfo, len(items))
|
byID := make(map[string]valuteInfo, len(items))
|
||||||
byNum := make(map[string]string, len(items))
|
byNum := make(map[string]string, len(items))
|
||||||
@@ -348,12 +412,19 @@ func buildValuteMapping(items []valuteItem) (*valuteMapping, error) {
|
|||||||
isoNum := strings.TrimSpace(item.ISONum)
|
isoNum := strings.TrimSpace(item.ISONum)
|
||||||
name := strings.TrimSpace(item.Name)
|
name := strings.TrimSpace(item.Name)
|
||||||
engName := strings.TrimSpace(item.EngName)
|
engName := strings.TrimSpace(item.EngName)
|
||||||
|
|
||||||
nominal, err := parseNominal(item.NominalStr)
|
nominal, err := parseNominal(item.NominalStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, merrors.InvalidDataType("cbr: parse directory nominal: " + err.Error())
|
return nil, merrors.InvalidDataType("cbr: parse directory nominal: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if id == "" || isoChar == "" {
|
if id == "" || isoChar == "" {
|
||||||
return nil, merrors.InvalidDataType("cbr: directory contains entry with empty id or iso code")
|
logger.Info("Skipping invalid currency entry",
|
||||||
|
zap.String("id", id),
|
||||||
|
zap.String("iso_char", isoChar),
|
||||||
|
zap.String("name", name),
|
||||||
|
)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
info := valuteInfo{
|
info := valuteInfo{
|
||||||
@@ -365,12 +436,60 @@ func buildValuteMapping(items []valuteItem) (*valuteMapping, error) {
|
|||||||
Nominal: nominal,
|
Nominal: nominal,
|
||||||
}
|
}
|
||||||
|
|
||||||
if existing, ok := byISO[isoChar]; ok && existing.ID != id {
|
// Handle duplicate ISO char codes (e.g. DEM with different IDs / nominals).
|
||||||
return nil, merrors.InvalidDataType("cbr: duplicate ISO code " + isoChar)
|
if existing, ok := byISO[isoChar]; ok {
|
||||||
|
// Same ISO + same ID: duplicate entry, just ignore.
|
||||||
|
if existing.ID == id {
|
||||||
|
logger.Debug("Duplicate directory entry for same ISO and ID, ignoring",
|
||||||
|
zap.String("iso_code", isoChar),
|
||||||
|
zap.String("id", id),
|
||||||
|
)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if existing, ok := byID[id]; ok && existing.ISOCharCode != isoChar {
|
|
||||||
return nil, merrors.InvalidDataType("cbr: duplicate valute id " + id)
|
// Different IDs but same ISO char.
|
||||||
|
// Choose canonical entry:
|
||||||
|
// 1) Prefer nominal == 1
|
||||||
|
// 2) Otherwise prefer smaller nominal
|
||||||
|
keepExisting := true
|
||||||
|
|
||||||
|
if existing.Nominal != 1 && info.Nominal == 1 {
|
||||||
|
keepExisting = false
|
||||||
|
} else if existing.Nominal == 1 && info.Nominal != 1 {
|
||||||
|
keepExisting = true
|
||||||
|
} else if info.Nominal < existing.Nominal {
|
||||||
|
keepExisting = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if keepExisting {
|
||||||
|
logger.Warn("Ignoring duplicate ISO currency entry with less preferred nominal",
|
||||||
|
zap.String("iso_code", isoChar),
|
||||||
|
zap.String("existing_id", existing.ID),
|
||||||
|
zap.Int64("existing_nominal", existing.Nominal),
|
||||||
|
zap.String("new_id", info.ID),
|
||||||
|
zap.Int64("new_nominal", info.Nominal),
|
||||||
|
)
|
||||||
|
// We keep the old one, just skip the new.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace existing mapping with the new, more canonical one.
|
||||||
|
logger.Warn("Replacing currency mapping due to more canonical nominal",
|
||||||
|
zap.String("iso_code", isoChar),
|
||||||
|
zap.String("old_id", existing.ID),
|
||||||
|
zap.Int64("old_nominal", existing.Nominal),
|
||||||
|
zap.String("new_id", info.ID),
|
||||||
|
zap.Int64("new_nominal", info.Nominal),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update byID: drop old ID, add new one
|
||||||
|
delete(byID, existing.ID)
|
||||||
|
byID[id] = info
|
||||||
|
|
||||||
|
// Update ISO mapping
|
||||||
|
byISO[isoChar] = info
|
||||||
|
|
||||||
|
// Update numeric-code index if present
|
||||||
if isoNum != "" {
|
if isoNum != "" {
|
||||||
if existingID, ok := byNum[isoNum]; ok && existingID != id {
|
if existingID, ok := byNum[isoNum]; ok && existingID != id {
|
||||||
return nil, merrors.InvalidDataType("cbr: duplicate ISO numeric code " + isoNum)
|
return nil, merrors.InvalidDataType("cbr: duplicate ISO numeric code " + isoNum)
|
||||||
@@ -378,6 +497,24 @@ func buildValuteMapping(items []valuteItem) (*valuteMapping, error) {
|
|||||||
byNum[isoNum] = id
|
byNum[isoNum] = id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// No existing ISO entry, do normal uniqueness checks.
|
||||||
|
|
||||||
|
if existing, ok := byID[id]; ok && existing.ISOCharCode != isoChar {
|
||||||
|
return nil, merrors.InvalidDataType("cbr: duplicate valute id " + id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isoNum != "" {
|
||||||
|
if existingID, ok := byNum[isoNum]; ok && existingID != id {
|
||||||
|
return nil, merrors.InvalidDataType("cbr: duplicate ISO numeric code " + isoNum)
|
||||||
|
}
|
||||||
|
byNum[isoNum] = id
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Installing currency code", zap.String("iso_code", isoChar), zap.String("id", id), zap.Int64("nominal", nominal))
|
||||||
|
|
||||||
byISO[isoChar] = info
|
byISO[isoChar] = info
|
||||||
byID[id] = info
|
byID[id] = info
|
||||||
}
|
}
|
||||||
|
|||||||
85
api/fx/ingestor/internal/market/cbr/http_client.go
Normal file
85
api/fx/ingestor/internal/market/cbr/http_client.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package cbr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultUserAgent = "Mozilla/5.0 (compatible; SendicoFX/1.0; +https://app.sendico.io)"
|
||||||
|
defaultAccept = "application/xml,text/xml;q=0.9,*/*;q=0.8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// httpClient wraps http.Client to ensure CBR requests always carry required headers.
|
||||||
|
type httpClient struct {
|
||||||
|
client *http.Client
|
||||||
|
headers http.Header
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpClientOptions struct {
|
||||||
|
userAgent string
|
||||||
|
accept string
|
||||||
|
referer string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPClient(logger *zap.Logger, client *http.Client, opts httpClientOptions) *httpClient {
|
||||||
|
userAgent := opts.userAgent
|
||||||
|
if strings.TrimSpace(userAgent) == "" {
|
||||||
|
userAgent = defaultUserAgent
|
||||||
|
}
|
||||||
|
|
||||||
|
accept := opts.accept
|
||||||
|
if strings.TrimSpace(accept) == "" {
|
||||||
|
accept = defaultAccept
|
||||||
|
}
|
||||||
|
|
||||||
|
referer := opts.referer
|
||||||
|
if strings.TrimSpace(referer) == "" {
|
||||||
|
referer = defaultCBRBaseURL
|
||||||
|
}
|
||||||
|
l := logger.Named("http_client")
|
||||||
|
|
||||||
|
headers := make(http.Header, 3)
|
||||||
|
headers.Set("User-Agent", userAgent)
|
||||||
|
headers.Set("Accept", accept)
|
||||||
|
headers.Set("Referer", referer)
|
||||||
|
|
||||||
|
l.Info("HTTP client initialized", zap.String("user_agent", userAgent),
|
||||||
|
zap.String("accept", accept), zap.String("referrer", referer))
|
||||||
|
|
||||||
|
return &httpClient{
|
||||||
|
client: client,
|
||||||
|
headers: headers,
|
||||||
|
logger: l,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) NewRequest(ctx context.Context, method, endpoint string) (*http.Request, error) {
|
||||||
|
return http.NewRequestWithContext(ctx, method, endpoint, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
enriched := req.Clone(req.Context())
|
||||||
|
for key, values := range h.headers {
|
||||||
|
if enriched.Header.Get(key) != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, value := range values {
|
||||||
|
enriched.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r, err := h.client.Do(enriched)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("HTTP request failed", zap.Error(err), zap.String("method", req.Method),
|
||||||
|
zap.String("url", req.URL.String()))
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) headerValue(name string) string {
|
||||||
|
return h.headers.Get(name)
|
||||||
|
}
|
||||||
@@ -40,15 +40,15 @@ func main() {
|
|||||||
|
|
||||||
application, err := app.New(logger, *configFile)
|
application, err := app.New(logger, *configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("Failed to initialise application", zap.Error(err))
|
logger.Error("Failed to initialise application", zap.Error(err))
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if err := application.Run(ctx); err != nil {
|
if err := application.Run(ctx); err != nil {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
logger.Info("FX ingestor stopped")
|
logger.Info("FX ingestor stopped")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Fatal("Ingestor terminated with error", zap.Error(err))
|
logger.Error("Ingestor terminated with error", zap.Error(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("FX ingestor stopped")
|
logger.Info("FX ingestor stopped")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ require (
|
|||||||
go.mongodb.org/mongo-driver v1.17.6
|
go.mongodb.org/mongo-driver v1.17.6
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
google.golang.org/grpc v1.77.0
|
google.golang.org/grpc v1.77.0
|
||||||
google.golang.org/protobuf v1.36.10
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.48.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
@@ -50,5 +50,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
@@ -212,12 +212,12 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
smodel "github.com/tech/sendico/pkg/model"
|
smodel "github.com/tech/sendico/pkg/model"
|
||||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
|
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
)
|
)
|
||||||
@@ -171,10 +170,13 @@ func buildQuoteMeta(meta *oraclev1.RequestMeta) *model.QuoteMeta {
|
|||||||
}
|
}
|
||||||
trace := meta.GetTrace()
|
trace := meta.GetTrace()
|
||||||
qm := &model.QuoteMeta{
|
qm := &model.QuoteMeta{
|
||||||
RequestRef: deriveRequestRef(meta, trace),
|
|
||||||
TenantRef: meta.GetTenantRef(),
|
TenantRef: meta.GetTenantRef(),
|
||||||
TraceRef: deriveTraceRef(meta, trace),
|
}
|
||||||
IdempotencyKey: deriveIdempotencyKey(meta, trace),
|
|
||||||
|
if trace != nil {
|
||||||
|
qm.RequestRef = trace.GetRequestRef()
|
||||||
|
qm.TraceRef = trace.GetTraceRef()
|
||||||
|
qm.IdempotencyKey = trace.GetIdempotencyKey()
|
||||||
}
|
}
|
||||||
if org := strings.TrimSpace(meta.GetOrganizationRef()); org != "" {
|
if org := strings.TrimSpace(meta.GetOrganizationRef()); org != "" {
|
||||||
if objID, err := primitive.ObjectIDFromHex(org); err == nil {
|
if objID, err := primitive.ObjectIDFromHex(org); err == nil {
|
||||||
@@ -201,24 +203,3 @@ func computeExpiry(now time.Time, ttlMs int64) (int64, error) {
|
|||||||
}
|
}
|
||||||
return now.Add(time.Duration(ttlMs) * time.Millisecond).UnixMilli(), nil
|
return now.Add(time.Duration(ttlMs) * time.Millisecond).UnixMilli(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deriveRequestRef(meta *oraclev1.RequestMeta, trace *tracev1.TraceContext) string {
|
|
||||||
if trace != nil && trace.GetRequestRef() != "" {
|
|
||||||
return trace.GetRequestRef()
|
|
||||||
}
|
|
||||||
return meta.GetRequestRef()
|
|
||||||
}
|
|
||||||
|
|
||||||
func deriveTraceRef(meta *oraclev1.RequestMeta, trace *tracev1.TraceContext) string {
|
|
||||||
if trace != nil && trace.GetTraceRef() != "" {
|
|
||||||
return trace.GetTraceRef()
|
|
||||||
}
|
|
||||||
return meta.GetTraceRef()
|
|
||||||
}
|
|
||||||
|
|
||||||
func deriveIdempotencyKey(meta *oraclev1.RequestMeta, trace *tracev1.TraceContext) string {
|
|
||||||
if trace != nil && trace.GetIdempotencyKey() != "" {
|
|
||||||
return trace.GetIdempotencyKey()
|
|
||||||
}
|
|
||||||
return meta.GetIdempotencyKey()
|
|
||||||
}
|
|
||||||
|
|||||||
90
api/fx/oracle/internal/service/oracle/logging.go
Normal file
90
api/fx/oracle/internal/service/oracle/logging.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package oracle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
|
||||||
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func quoteRequestFields(req *oraclev1.GetQuoteRequest) []zap.Field {
|
||||||
|
if req == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fields := requestMetaFields(req.GetMeta())
|
||||||
|
if pair := req.GetPair(); pair != nil {
|
||||||
|
if base := strings.TrimSpace(pair.GetBase()); base != "" {
|
||||||
|
fields = append(fields, zap.String("pair_base", base))
|
||||||
|
}
|
||||||
|
if quote := strings.TrimSpace(pair.GetQuote()); quote != "" {
|
||||||
|
fields = append(fields, zap.String("pair_quote", quote))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if side := req.GetSide(); side != fxv1.Side_SIDE_UNSPECIFIED {
|
||||||
|
fields = append(fields, zap.String("side", side.String()))
|
||||||
|
}
|
||||||
|
if req.GetFirm() {
|
||||||
|
fields = append(fields, zap.Bool("firm", req.GetFirm()))
|
||||||
|
}
|
||||||
|
if ttl := req.GetTtlMs(); ttl > 0 {
|
||||||
|
fields = append(fields, zap.Int64("ttl_ms", ttl))
|
||||||
|
}
|
||||||
|
if maxAge := req.GetMaxAgeMs(); maxAge > 0 {
|
||||||
|
fields = append(fields, zap.Int32("max_age_ms", maxAge))
|
||||||
|
}
|
||||||
|
if provider := strings.TrimSpace(req.GetPreferredProvider()); provider != "" {
|
||||||
|
fields = append(fields, zap.String("preferred_provider", provider))
|
||||||
|
}
|
||||||
|
fields = append(fields, moneyFields("base_amount", req.GetBaseAmount())...)
|
||||||
|
fields = append(fields, moneyFields("quote_amount", req.GetQuoteAmount())...)
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestMetaFields(meta *oraclev1.RequestMeta) []zap.Field {
|
||||||
|
if meta == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fields := make([]zap.Field, 0, 4)
|
||||||
|
if org := strings.TrimSpace(meta.GetOrganizationRef()); org != "" {
|
||||||
|
fields = append(fields, zap.String("organization_ref", org))
|
||||||
|
}
|
||||||
|
if tenant := strings.TrimSpace(meta.GetTenantRef()); tenant != "" {
|
||||||
|
fields = append(fields, zap.String("tenant_ref", tenant))
|
||||||
|
}
|
||||||
|
fields = append(fields, traceFields(meta.GetTrace())...)
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func moneyFields(prefix string, money *moneyv1.Money) []zap.Field {
|
||||||
|
if money == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fields := make([]zap.Field, 0, 2)
|
||||||
|
if amt := strings.TrimSpace(money.GetAmount()); amt != "" {
|
||||||
|
fields = append(fields, zap.String(prefix, amt))
|
||||||
|
}
|
||||||
|
if ccy := strings.TrimSpace(money.GetCurrency()); ccy != "" {
|
||||||
|
fields = append(fields, zap.String(prefix+"_currency", ccy))
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceFields(trace *tracev1.TraceContext) []zap.Field {
|
||||||
|
if trace == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fields := make([]zap.Field, 0, 3)
|
||||||
|
if ref := strings.TrimSpace(trace.GetTraceRef()); ref != "" {
|
||||||
|
fields = append(fields, zap.String("trace_ref", ref))
|
||||||
|
}
|
||||||
|
if idem := strings.TrimSpace(trace.GetIdempotencyKey()); idem != "" {
|
||||||
|
fields = append(fields, zap.String("idempotency_key", idem))
|
||||||
|
}
|
||||||
|
if req := strings.TrimSpace(trace.GetRequestRef()); req != "" {
|
||||||
|
fields = append(fields, zap.String("request_ref", req))
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package oracle
|
|||||||
import (
|
import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tech/sendico/fx/storage/model"
|
"github.com/tech/sendico/fx/storage/model"
|
||||||
"github.com/tech/sendico/pkg/decimal"
|
"github.com/tech/sendico/pkg/decimal"
|
||||||
@@ -61,7 +60,3 @@ func priceFromRate(rate *model.RateSnapshot, side fxv1.Side) (*big.Rat, error) {
|
|||||||
|
|
||||||
return ratFromString(priceStr)
|
return ratFromString(priceStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func timeFromUnixMilli(ms int64) time.Time {
|
|
||||||
return time.Unix(0, ms*int64(time.Millisecond))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -101,22 +101,27 @@ func (s *Service) getQuoteResponder(ctx context.Context, req *oraclev1.GetQuoteR
|
|||||||
if req == nil {
|
if req == nil {
|
||||||
req = &oraclev1.GetQuoteRequest{}
|
req = &oraclev1.GetQuoteRequest{}
|
||||||
}
|
}
|
||||||
s.logger.Debug("Handling GetQuote", zap.String("pair", req.GetPair().GetBase()+"/"+req.GetPair().GetQuote()), zap.Bool("firm", req.GetFirm()))
|
logger := s.logger.With(quoteRequestFields(req)...)
|
||||||
|
logger.Debug("Handling GetQuote")
|
||||||
if req.GetSide() == fxv1.Side_SIDE_UNSPECIFIED {
|
if req.GetSide() == fxv1.Side_SIDE_UNSPECIFIED {
|
||||||
|
logger.Warn("GetQuote invalid: side missing")
|
||||||
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, errSideRequired)
|
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, errSideRequired)
|
||||||
}
|
}
|
||||||
if req.GetBaseAmount() != nil && req.GetQuoteAmount() != nil {
|
if req.GetBaseAmount() != nil && req.GetQuoteAmount() != nil {
|
||||||
|
logger.Warn("GetQuote invalid: both base_amount and quote_amount provided")
|
||||||
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, errAmountsMutuallyExclusive)
|
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, errAmountsMutuallyExclusive)
|
||||||
}
|
}
|
||||||
if req.GetBaseAmount() == nil && req.GetQuoteAmount() == nil {
|
if req.GetBaseAmount() == nil && req.GetQuoteAmount() == nil {
|
||||||
|
logger.Warn("GetQuote invalid: amount missing")
|
||||||
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, errAmountRequired)
|
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, errAmountRequired)
|
||||||
}
|
}
|
||||||
if err := s.pingStorage(ctx); err != nil {
|
if err := s.pingStorage(ctx); err != nil {
|
||||||
s.logger.Warn("Storage unavailable during GetQuote", zap.Error(err))
|
logger.Warn("Storage unavailable during GetQuote", zap.Error(err))
|
||||||
return gsresponse.Unavailable[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Unavailable[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
pairMsg := req.GetPair()
|
pairMsg := req.GetPair()
|
||||||
if pairMsg == nil || strings.TrimSpace(pairMsg.GetBase()) == "" || strings.TrimSpace(pairMsg.GetQuote()) == "" {
|
if pairMsg == nil || strings.TrimSpace(pairMsg.GetBase()) == "" || strings.TrimSpace(pairMsg.GetQuote()) == "" {
|
||||||
|
logger.Warn("GetQuote invalid: pair missing")
|
||||||
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, errEmptyRequest)
|
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, errEmptyRequest)
|
||||||
}
|
}
|
||||||
pairKey := model.CurrencyPair{Base: strings.ToUpper(pairMsg.GetBase()), Quote: strings.ToUpper(pairMsg.GetQuote())}
|
pairKey := model.CurrencyPair{Base: strings.ToUpper(pairMsg.GetBase()), Quote: strings.ToUpper(pairMsg.GetQuote())}
|
||||||
@@ -125,8 +130,10 @@ func (s *Service) getQuoteResponder(ctx context.Context, req *oraclev1.GetQuoteR
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, merrors.ErrNoData):
|
case errors.Is(err, merrors.ErrNoData):
|
||||||
|
logger.Warn("pair not supported", zap.String("pair", pairKey.Base+"/"+pairKey.Quote))
|
||||||
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, merrors.InvalidArgument("pair_not_supported"))
|
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, merrors.InvalidArgument("pair_not_supported"))
|
||||||
default:
|
default:
|
||||||
|
logger.Warn("GetQuote failed to load pair", zap.Error(err))
|
||||||
return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,8 +150,10 @@ func (s *Service) getQuoteResponder(ctx context.Context, req *oraclev1.GetQuoteR
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, merrors.ErrNoData):
|
case errors.Is(err, merrors.ErrNoData):
|
||||||
|
logger.Warn("rate not found", zap.String("pair", pairKey.Base+"/"+pairKey.Quote), zap.String("provider", provider))
|
||||||
return gsresponse.FailedPrecondition[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, "rate_not_found", err)
|
return gsresponse.FailedPrecondition[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, "rate_not_found", err)
|
||||||
default:
|
default:
|
||||||
|
logger.Warn("GetQuote failed to load rate", zap.Error(err), zap.String("pair", pairKey.Base+"/"+pairKey.Quote), zap.String("provider", provider))
|
||||||
return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,27 +162,31 @@ func (s *Service) getQuoteResponder(ctx context.Context, req *oraclev1.GetQuoteR
|
|||||||
if maxAge := req.GetMaxAgeMs(); maxAge > 0 {
|
if maxAge := req.GetMaxAgeMs(); maxAge > 0 {
|
||||||
age := now.UnixMilli() - rate.AsOfUnixMs
|
age := now.UnixMilli() - rate.AsOfUnixMs
|
||||||
if age > int64(maxAge) {
|
if age > int64(maxAge) {
|
||||||
s.logger.Warn("Rate snapshot stale", zap.Int64("age_ms", age), zap.Int32("max_age_ms", req.GetMaxAgeMs()), zap.String("pair", pairKey.Base+"/"+pairKey.Quote), zap.String("provider", provider))
|
logger.Warn("Rate snapshot stale", zap.Int64("age_ms", age), zap.Int32("max_age_ms", req.GetMaxAgeMs()), zap.String("pair", pairKey.Base+"/"+pairKey.Quote), zap.String("provider", provider))
|
||||||
return gsresponse.FailedPrecondition[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, "stale_rate", merrors.InvalidArgument("rate older than allowed window"))
|
return gsresponse.FailedPrecondition[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, "stale_rate", merrors.InvalidArgument("rate older than allowed window"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
comp, err := newQuoteComputation(pair, rate, req.GetSide(), provider)
|
comp, err := newQuoteComputation(pair, rate, req.GetSide(), provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Warn("GetQuote invalid input", zap.Error(err))
|
||||||
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.GetBaseAmount() != nil {
|
if req.GetBaseAmount() != nil {
|
||||||
if err := comp.withBaseInput(req.GetBaseAmount()); err != nil {
|
if err := comp.withBaseInput(req.GetBaseAmount()); err != nil {
|
||||||
|
logger.Warn("GetQuote invalid base input", zap.Error(err))
|
||||||
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
} else if req.GetQuoteAmount() != nil {
|
} else if req.GetQuoteAmount() != nil {
|
||||||
if err := comp.withQuoteInput(req.GetQuoteAmount()); err != nil {
|
if err := comp.withQuoteInput(req.GetQuoteAmount()); err != nil {
|
||||||
|
logger.Warn("GetQuote invalid quote input", zap.Error(err))
|
||||||
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := comp.compute(); err != nil {
|
if err := comp.compute(); err != nil {
|
||||||
|
logger.Warn("GetQuote computation failed", zap.Error(err))
|
||||||
return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,12 +208,14 @@ func (s *Service) getQuoteResponder(ctx context.Context, req *oraclev1.GetQuoteR
|
|||||||
if err := s.storage.Quotes().Issue(ctx, quoteModel); err != nil {
|
if err := s.storage.Quotes().Issue(ctx, quoteModel); err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, merrors.ErrDataConflict):
|
case errors.Is(err, merrors.ErrDataConflict):
|
||||||
|
logger.Warn("GetQuote conflict issuing firm quote", zap.Error(err), zap.String("quote_ref", quoteModel.QuoteRef))
|
||||||
return gsresponse.Conflict[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Conflict[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
default:
|
default:
|
||||||
|
logger.Warn("GetQuote failed to issue firm quote", zap.Error(err), zap.String("quote_ref", quoteModel.QuoteRef))
|
||||||
return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.logger.Info("Firm quote stored", zap.String("quote_ref", quoteModel.QuoteRef), zap.String("pair", pairKey.Base+"/"+pairKey.Quote), zap.String("provider", quoteModel.Provider), zap.Int64("expires_at_ms", quoteModel.ExpiresAtUnixMs))
|
logger.Info("Firm quote stored", zap.String("quote_ref", quoteModel.QuoteRef), zap.String("pair", pairKey.Base+"/"+pairKey.Quote), zap.String("provider", quoteModel.Provider), zap.Int64("expires_at_ms", quoteModel.ExpiresAtUnixMs))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := &oraclev1.GetQuoteResponse{
|
resp := &oraclev1.GetQuoteResponse{
|
||||||
@@ -214,18 +229,24 @@ func (s *Service) validateQuoteResponder(ctx context.Context, req *oraclev1.Vali
|
|||||||
if req == nil {
|
if req == nil {
|
||||||
req = &oraclev1.ValidateQuoteRequest{}
|
req = &oraclev1.ValidateQuoteRequest{}
|
||||||
}
|
}
|
||||||
s.logger.Debug("Handling ValidateQuote", zap.String("quote_ref", req.GetQuoteRef()))
|
logger := s.logger.With(requestMetaFields(req.GetMeta())...)
|
||||||
|
if ref := strings.TrimSpace(req.GetQuoteRef()); ref != "" {
|
||||||
|
logger = logger.With(zap.String("quote_ref", ref))
|
||||||
|
}
|
||||||
|
logger.Debug("Handling ValidateQuote")
|
||||||
if req.GetQuoteRef() == "" {
|
if req.GetQuoteRef() == "" {
|
||||||
|
logger.Warn("ValidateQuote invalid: quote_ref missing")
|
||||||
return gsresponse.InvalidArgument[oraclev1.ValidateQuoteResponse](s.logger, mservice.FXOracle, errQuoteRefRequired)
|
return gsresponse.InvalidArgument[oraclev1.ValidateQuoteResponse](s.logger, mservice.FXOracle, errQuoteRefRequired)
|
||||||
}
|
}
|
||||||
if err := s.pingStorage(ctx); err != nil {
|
if err := s.pingStorage(ctx); err != nil {
|
||||||
s.logger.Warn("Storage unavailable during ValidateQuote", zap.Error(err))
|
logger.Warn("Storage unavailable during ValidateQuote", zap.Error(err))
|
||||||
return gsresponse.Unavailable[oraclev1.ValidateQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Unavailable[oraclev1.ValidateQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
quote, err := s.storage.Quotes().GetByRef(ctx, req.GetQuoteRef())
|
quote, err := s.storage.Quotes().GetByRef(ctx, req.GetQuoteRef())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, merrors.ErrNoData):
|
case errors.Is(err, merrors.ErrNoData):
|
||||||
|
logger.Warn("ValidateQuote: quote not found", zap.String("quote_ref", req.GetQuoteRef()))
|
||||||
resp := &oraclev1.ValidateQuoteResponse{
|
resp := &oraclev1.ValidateQuoteResponse{
|
||||||
Meta: buildResponseMeta(req.GetMeta()),
|
Meta: buildResponseMeta(req.GetMeta()),
|
||||||
Quote: nil,
|
Quote: nil,
|
||||||
@@ -234,6 +255,7 @@ func (s *Service) validateQuoteResponder(ctx context.Context, req *oraclev1.Vali
|
|||||||
}
|
}
|
||||||
return gsresponse.Success(resp)
|
return gsresponse.Success(resp)
|
||||||
default:
|
default:
|
||||||
|
logger.Warn("ValidateQuote failed", zap.Error(err))
|
||||||
return gsresponse.Internal[oraclev1.ValidateQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Internal[oraclev1.ValidateQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,6 +277,11 @@ func (s *Service) validateQuoteResponder(ctx context.Context, req *oraclev1.Vali
|
|||||||
Valid: valid,
|
Valid: valid,
|
||||||
Reason: reason,
|
Reason: reason,
|
||||||
}
|
}
|
||||||
|
if !valid {
|
||||||
|
logger.Info("ValidateQuote invalid", zap.String("reason", reason), zap.Bool("firm", quote.Firm))
|
||||||
|
} else {
|
||||||
|
logger.Debug("ValidateQuote valid", zap.Bool("firm", quote.Firm))
|
||||||
|
}
|
||||||
return gsresponse.Success(resp)
|
return gsresponse.Success(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,29 +289,43 @@ func (s *Service) consumeQuoteResponder(ctx context.Context, req *oraclev1.Consu
|
|||||||
if req == nil {
|
if req == nil {
|
||||||
req = &oraclev1.ConsumeQuoteRequest{}
|
req = &oraclev1.ConsumeQuoteRequest{}
|
||||||
}
|
}
|
||||||
s.logger.Debug("Handling ConsumeQuote", zap.String("quote_ref", req.GetQuoteRef()), zap.String("ledger_txn_ref", req.GetLedgerTxnRef()))
|
logger := s.logger.With(requestMetaFields(req.GetMeta())...)
|
||||||
|
if ref := strings.TrimSpace(req.GetQuoteRef()); ref != "" {
|
||||||
|
logger = logger.With(zap.String("quote_ref", ref))
|
||||||
|
}
|
||||||
|
if ledger := strings.TrimSpace(req.GetLedgerTxnRef()); ledger != "" {
|
||||||
|
logger = logger.With(zap.String("ledger_txn_ref", ledger))
|
||||||
|
}
|
||||||
|
logger.Debug("Handling ConsumeQuote")
|
||||||
if req.GetQuoteRef() == "" {
|
if req.GetQuoteRef() == "" {
|
||||||
|
logger.Warn("ConsumeQuote invalid: quote_ref missing")
|
||||||
return gsresponse.InvalidArgument[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, errQuoteRefRequired)
|
return gsresponse.InvalidArgument[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, errQuoteRefRequired)
|
||||||
}
|
}
|
||||||
if req.GetLedgerTxnRef() == "" {
|
if req.GetLedgerTxnRef() == "" {
|
||||||
|
logger.Warn("ConsumeQuote invalid: ledger_txn_ref missing")
|
||||||
return gsresponse.InvalidArgument[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, errLedgerTxnRefRequired)
|
return gsresponse.InvalidArgument[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, errLedgerTxnRefRequired)
|
||||||
}
|
}
|
||||||
if err := s.pingStorage(ctx); err != nil {
|
if err := s.pingStorage(ctx); err != nil {
|
||||||
s.logger.Warn("Storage unavailable during ConsumeQuote", zap.Error(err))
|
logger.Warn("Storage unavailable during ConsumeQuote", zap.Error(err))
|
||||||
return gsresponse.Unavailable[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Unavailable[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
_, err := s.storage.Quotes().Consume(ctx, req.GetQuoteRef(), req.GetLedgerTxnRef(), time.Now())
|
_, err := s.storage.Quotes().Consume(ctx, req.GetQuoteRef(), req.GetLedgerTxnRef(), time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, storage.ErrQuoteExpired):
|
case errors.Is(err, storage.ErrQuoteExpired):
|
||||||
|
logger.Warn("ConsumeQuote failed: expired")
|
||||||
return gsresponse.FailedPrecondition[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, "expired", err)
|
return gsresponse.FailedPrecondition[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, "expired", err)
|
||||||
case errors.Is(err, storage.ErrQuoteConsumed):
|
case errors.Is(err, storage.ErrQuoteConsumed):
|
||||||
|
logger.Warn("ConsumeQuote failed: already consumed")
|
||||||
return gsresponse.FailedPrecondition[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, "consumed", err)
|
return gsresponse.FailedPrecondition[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, "consumed", err)
|
||||||
case errors.Is(err, storage.ErrQuoteNotFirm):
|
case errors.Is(err, storage.ErrQuoteNotFirm):
|
||||||
|
logger.Warn("ConsumeQuote failed: quote not firm")
|
||||||
return gsresponse.FailedPrecondition[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, "not_firm", err)
|
return gsresponse.FailedPrecondition[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, "not_firm", err)
|
||||||
case errors.Is(err, merrors.ErrNoData):
|
case errors.Is(err, merrors.ErrNoData):
|
||||||
|
logger.Warn("ConsumeQuote failed: quote not found")
|
||||||
return gsresponse.NotFound[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.NotFound[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
default:
|
default:
|
||||||
|
logger.Warn("ConsumeQuote failed", zap.Error(err))
|
||||||
return gsresponse.Internal[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Internal[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,7 +335,7 @@ func (s *Service) consumeQuoteResponder(ctx context.Context, req *oraclev1.Consu
|
|||||||
Consumed: true,
|
Consumed: true,
|
||||||
Reason: "consumed",
|
Reason: "consumed",
|
||||||
}
|
}
|
||||||
s.logger.Debug("Quote consumed", zap.String("quote_ref", req.GetQuoteRef()), zap.String("ledger_txn_ref", req.GetLedgerTxnRef()))
|
logger.Info("Quote consumed")
|
||||||
return gsresponse.Success(resp)
|
return gsresponse.Success(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,13 +343,21 @@ func (s *Service) latestRateResponder(ctx context.Context, req *oraclev1.LatestR
|
|||||||
if req == nil {
|
if req == nil {
|
||||||
req = &oraclev1.LatestRateRequest{}
|
req = &oraclev1.LatestRateRequest{}
|
||||||
}
|
}
|
||||||
s.logger.Debug("Handling LatestRate", zap.String("pair", req.GetPair().GetBase()+"/"+req.GetPair().GetQuote()))
|
logger := s.logger.With(requestMetaFields(req.GetMeta())...)
|
||||||
|
if pair := req.GetPair(); pair != nil {
|
||||||
|
logger = logger.With(zap.String("pair_base", strings.TrimSpace(pair.GetBase())), zap.String("pair_quote", strings.TrimSpace(pair.GetQuote())))
|
||||||
|
}
|
||||||
|
if provider := strings.TrimSpace(req.GetProvider()); provider != "" {
|
||||||
|
logger = logger.With(zap.String("provider", provider))
|
||||||
|
}
|
||||||
|
logger.Debug("Handling LatestRate")
|
||||||
if err := s.pingStorage(ctx); err != nil {
|
if err := s.pingStorage(ctx); err != nil {
|
||||||
s.logger.Warn("Storage unavailable during LatestRate", zap.Error(err))
|
logger.Warn("Storage unavailable during LatestRate", zap.Error(err))
|
||||||
return gsresponse.Unavailable[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Unavailable[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
pairMsg := req.GetPair()
|
pairMsg := req.GetPair()
|
||||||
if pairMsg == nil || strings.TrimSpace(pairMsg.GetBase()) == "" || strings.TrimSpace(pairMsg.GetQuote()) == "" {
|
if pairMsg == nil || strings.TrimSpace(pairMsg.GetBase()) == "" || strings.TrimSpace(pairMsg.GetQuote()) == "" {
|
||||||
|
logger.Warn("LatestRate invalid: pair missing")
|
||||||
return gsresponse.InvalidArgument[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, errEmptyRequest)
|
return gsresponse.InvalidArgument[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, errEmptyRequest)
|
||||||
}
|
}
|
||||||
pair := model.CurrencyPair{Base: strings.ToUpper(pairMsg.GetBase()), Quote: strings.ToUpper(pairMsg.GetQuote())}
|
pair := model.CurrencyPair{Base: strings.ToUpper(pairMsg.GetBase()), Quote: strings.ToUpper(pairMsg.GetQuote())}
|
||||||
@@ -317,8 +366,10 @@ func (s *Service) latestRateResponder(ctx context.Context, req *oraclev1.LatestR
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, merrors.ErrNoData):
|
case errors.Is(err, merrors.ErrNoData):
|
||||||
|
logger.Warn("LatestRate pair not found")
|
||||||
return gsresponse.NotFound[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.NotFound[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err)
|
||||||
default:
|
default:
|
||||||
|
logger.Warn("LatestRate failed to load pair", zap.Error(err))
|
||||||
return gsresponse.Internal[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Internal[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,8 +386,10 @@ func (s *Service) latestRateResponder(ctx context.Context, req *oraclev1.LatestR
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, merrors.ErrNoData):
|
case errors.Is(err, merrors.ErrNoData):
|
||||||
|
logger.Warn("LatestRate not found", zap.String("provider", provider))
|
||||||
return gsresponse.NotFound[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.NotFound[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err)
|
||||||
default:
|
default:
|
||||||
|
logger.Warn("LatestRate failed", zap.Error(err))
|
||||||
return gsresponse.Internal[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Internal[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,6 +398,7 @@ func (s *Service) latestRateResponder(ctx context.Context, req *oraclev1.LatestR
|
|||||||
Meta: buildResponseMeta(req.GetMeta()),
|
Meta: buildResponseMeta(req.GetMeta()),
|
||||||
Rate: rateModelToProto(rate),
|
Rate: rateModelToProto(rate),
|
||||||
}
|
}
|
||||||
|
logger.Debug("LatestRate succeeded", zap.String("provider", provider), zap.Int64("asof_unix_ms", rate.AsOfUnixMs))
|
||||||
return gsresponse.Success(resp)
|
return gsresponse.Success(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,13 +406,15 @@ func (s *Service) listPairsResponder(ctx context.Context, req *oraclev1.ListPair
|
|||||||
if req == nil {
|
if req == nil {
|
||||||
req = &oraclev1.ListPairsRequest{}
|
req = &oraclev1.ListPairsRequest{}
|
||||||
}
|
}
|
||||||
s.logger.Debug("Handling ListPairs")
|
logger := s.logger.With(requestMetaFields(req.GetMeta())...)
|
||||||
|
logger.Debug("Handling ListPairs")
|
||||||
if err := s.pingStorage(ctx); err != nil {
|
if err := s.pingStorage(ctx); err != nil {
|
||||||
s.logger.Warn("Storage unavailable during ListPairs", zap.Error(err))
|
logger.Warn("Storage unavailable during ListPairs", zap.Error(err))
|
||||||
return gsresponse.Unavailable[oraclev1.ListPairsResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Unavailable[oraclev1.ListPairsResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
pairs, err := s.storage.Pairs().ListEnabled(ctx)
|
pairs, err := s.storage.Pairs().ListEnabled(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Warn("ListPairs failed", zap.Error(err))
|
||||||
return gsresponse.Internal[oraclev1.ListPairsResponse](s.logger, mservice.FXOracle, err)
|
return gsresponse.Internal[oraclev1.ListPairsResponse](s.logger, mservice.FXOracle, err)
|
||||||
}
|
}
|
||||||
result := make([]*oraclev1.PairMeta, 0, len(pairs))
|
result := make([]*oraclev1.PairMeta, 0, len(pairs))
|
||||||
@@ -369,7 +425,7 @@ func (s *Service) listPairsResponder(ctx context.Context, req *oraclev1.ListPair
|
|||||||
Meta: buildResponseMeta(req.GetMeta()),
|
Meta: buildResponseMeta(req.GetMeta()),
|
||||||
Pairs: result,
|
Pairs: result,
|
||||||
}
|
}
|
||||||
s.logger.Debug("ListPairs returning metadata", zap.Int("pairs", len(resp.GetPairs())))
|
logger.Debug("ListPairs returning metadata", zap.Int("pairs", len(resp.GetPairs())))
|
||||||
return gsresponse.Success(resp)
|
return gsresponse.Success(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
smodel "github.com/tech/sendico/pkg/model"
|
smodel "github.com/tech/sendico/pkg/model"
|
||||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
|
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,18 +15,11 @@ func buildResponseMeta(meta *oraclev1.RequestMeta) *oraclev1.ResponseMeta {
|
|||||||
if meta == nil {
|
if meta == nil {
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
resp.RequestRef = meta.GetRequestRef()
|
|
||||||
resp.TraceRef = meta.GetTraceRef()
|
|
||||||
|
|
||||||
trace := meta.GetTrace()
|
trace := meta.GetTrace()
|
||||||
if trace == nil {
|
if trace != nil {
|
||||||
trace = &tracev1.TraceContext{
|
|
||||||
RequestRef: meta.GetRequestRef(),
|
|
||||||
IdempotencyKey: meta.GetIdempotencyKey(),
|
|
||||||
TraceRef: meta.GetTraceRef(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.Trace = trace
|
resp.Trace = trace
|
||||||
|
}
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,5 +28,5 @@ require (
|
|||||||
golang.org/x/crypto v0.46.0 // indirect
|
golang.org/x/crypto v0.46.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBn
|
|||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ require (
|
|||||||
go.mongodb.org/mongo-driver v1.17.6
|
go.mongodb.org/mongo-driver v1.17.6
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
google.golang.org/grpc v1.77.0
|
google.golang.org/grpc v1.77.0
|
||||||
google.golang.org/protobuf v1.36.10
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251208031133-be43a854e4be // indirect
|
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251213223233-751f36331c62 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||||
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
|
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
|
||||||
@@ -60,7 +60,7 @@ require (
|
|||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.48.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
@@ -86,5 +86,5 @@ require (
|
|||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
|||||||
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251208031133-be43a854e4be h1:1LtMLkGIqE5IQZ7Vdh4zv8A6LECInKF86/fTVxKxYLE=
|
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251213223233-751f36331c62 h1:Rge3uIIO891+nLqKTfMulCw+tWHtTl16Oudi0yUcAoE=
|
||||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251208031133-be43a854e4be/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251213223233-751f36331c62/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||||
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
|
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
|
||||||
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
|
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
@@ -207,8 +207,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
@@ -362,12 +362,12 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ require (
|
|||||||
github.com/tech/sendico/pkg v0.1.0
|
github.com/tech/sendico/pkg v0.1.0
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
google.golang.org/grpc v1.77.0
|
google.golang.org/grpc v1.77.0
|
||||||
google.golang.org/protobuf v1.36.10
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.48.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
@@ -50,5 +50,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
@@ -214,12 +214,12 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ require (
|
|||||||
go.mongodb.org/mongo-driver v1.17.6
|
go.mongodb.org/mongo-driver v1.17.6
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
google.golang.org/grpc v1.77.0
|
google.golang.org/grpc v1.77.0
|
||||||
google.golang.org/protobuf v1.36.10
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.48.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
@@ -51,5 +51,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
@@ -214,12 +214,12 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.48.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
@@ -52,7 +52,7 @@ require (
|
|||||||
golang.org/x/net v0.48.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||||
google.golang.org/grpc v1.77.0 // indirect
|
google.golang.org/grpc v1.77.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
@@ -227,12 +227,12 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import (
|
|||||||
// Client exposes typed helpers around the payment orchestrator gRPC API.
|
// Client exposes typed helpers around the payment orchestrator gRPC API.
|
||||||
type Client interface {
|
type Client interface {
|
||||||
QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error)
|
QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error)
|
||||||
|
QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error)
|
||||||
|
InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
|
||||||
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
|
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
|
||||||
CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error)
|
CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error)
|
||||||
GetPayment(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error)
|
GetPayment(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error)
|
||||||
@@ -29,6 +31,8 @@ type Client interface {
|
|||||||
|
|
||||||
type grpcOrchestratorClient interface {
|
type grpcOrchestratorClient interface {
|
||||||
QuotePayment(ctx context.Context, in *orchestratorv1.QuotePaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.QuotePaymentResponse, error)
|
QuotePayment(ctx context.Context, in *orchestratorv1.QuotePaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.QuotePaymentResponse, error)
|
||||||
|
QuotePayments(ctx context.Context, in *orchestratorv1.QuotePaymentsRequest, opts ...grpc.CallOption) (*orchestratorv1.QuotePaymentsResponse, error)
|
||||||
|
InitiatePayments(ctx context.Context, in *orchestratorv1.InitiatePaymentsRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiatePaymentsResponse, error)
|
||||||
InitiatePayment(ctx context.Context, in *orchestratorv1.InitiatePaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiatePaymentResponse, error)
|
InitiatePayment(ctx context.Context, in *orchestratorv1.InitiatePaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiatePaymentResponse, error)
|
||||||
CancelPayment(ctx context.Context, in *orchestratorv1.CancelPaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.CancelPaymentResponse, error)
|
CancelPayment(ctx context.Context, in *orchestratorv1.CancelPaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.CancelPaymentResponse, error)
|
||||||
GetPayment(ctx context.Context, in *orchestratorv1.GetPaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.GetPaymentResponse, error)
|
GetPayment(ctx context.Context, in *orchestratorv1.GetPaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.GetPaymentResponse, error)
|
||||||
@@ -97,6 +101,18 @@ func (c *orchestratorClient) QuotePayment(ctx context.Context, req *orchestrator
|
|||||||
return c.client.QuotePayment(ctx, req)
|
return c.client.QuotePayment(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *orchestratorClient) QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error) {
|
||||||
|
ctx, cancel := c.callContext(ctx)
|
||||||
|
defer cancel()
|
||||||
|
return c.client.QuotePayments(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *orchestratorClient) InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error) {
|
||||||
|
ctx, cancel := c.callContext(ctx)
|
||||||
|
defer cancel()
|
||||||
|
return c.client.InitiatePayments(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *orchestratorClient) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) {
|
func (c *orchestratorClient) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
// Fake implements Client for tests.
|
// Fake implements Client for tests.
|
||||||
type Fake struct {
|
type Fake struct {
|
||||||
QuotePaymentFn func(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error)
|
QuotePaymentFn func(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error)
|
||||||
|
QuotePaymentsFn func(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error)
|
||||||
|
InitiatePaymentsFn func(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
|
||||||
InitiatePaymentFn func(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
|
InitiatePaymentFn func(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
|
||||||
CancelPaymentFn func(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error)
|
CancelPaymentFn func(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error)
|
||||||
GetPaymentFn func(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error)
|
GetPaymentFn func(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error)
|
||||||
@@ -26,6 +28,20 @@ func (f *Fake) QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymen
|
|||||||
return &orchestratorv1.QuotePaymentResponse{}, nil
|
return &orchestratorv1.QuotePaymentResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fake) QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error) {
|
||||||
|
if f.QuotePaymentsFn != nil {
|
||||||
|
return f.QuotePaymentsFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &orchestratorv1.QuotePaymentsResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error) {
|
||||||
|
if f.InitiatePaymentsFn != nil {
|
||||||
|
return f.InitiatePaymentsFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &orchestratorv1.InitiatePaymentsResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fake) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) {
|
func (f *Fake) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) {
|
||||||
if f.InitiatePaymentFn != nil {
|
if f.InitiatePaymentFn != nil {
|
||||||
return f.InitiatePaymentFn(ctx, req)
|
return f.InitiatePaymentFn(ctx, req)
|
||||||
|
|||||||
@@ -61,3 +61,6 @@ card_gateways:
|
|||||||
monetix:
|
monetix:
|
||||||
funding_address: "wallet_funding_monetix"
|
funding_address: "wallet_funding_monetix"
|
||||||
fee_address: "wallet_fee_monetix"
|
fee_address: "wallet_fee_monetix"
|
||||||
|
|
||||||
|
fee_ledger_accounts:
|
||||||
|
monetix: "ledger:fees:monetix"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ require (
|
|||||||
go.mongodb.org/mongo-driver v1.17.6
|
go.mongodb.org/mongo-driver v1.17.6
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
google.golang.org/grpc v1.77.0
|
google.golang.org/grpc v1.77.0
|
||||||
google.golang.org/protobuf v1.36.10
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.48.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
@@ -62,5 +62,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
@@ -215,12 +215,12 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type config struct {
|
|||||||
Gateway clientConfig `yaml:"gateway"`
|
Gateway clientConfig `yaml:"gateway"`
|
||||||
Oracle clientConfig `yaml:"oracle"`
|
Oracle clientConfig `yaml:"oracle"`
|
||||||
CardGateways map[string]cardGatewayRouteConfig `yaml:"card_gateways"`
|
CardGateways map[string]cardGatewayRouteConfig `yaml:"card_gateways"`
|
||||||
|
FeeAccounts map[string]string `yaml:"fee_ledger_accounts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientConfig struct {
|
type clientConfig struct {
|
||||||
@@ -159,6 +160,9 @@ func (i *Imp) Start() error {
|
|||||||
if routes := buildCardGatewayRoutes(cfg.CardGateways); len(routes) > 0 {
|
if routes := buildCardGatewayRoutes(cfg.CardGateways); len(routes) > 0 {
|
||||||
opts = append(opts, orchestrator.WithCardGatewayRoutes(routes))
|
opts = append(opts, orchestrator.WithCardGatewayRoutes(routes))
|
||||||
}
|
}
|
||||||
|
if feeAccounts := buildFeeLedgerAccounts(cfg.FeeAccounts); len(feeAccounts) > 0 {
|
||||||
|
opts = append(opts, orchestrator.WithFeeLedgerAccounts(feeAccounts))
|
||||||
|
}
|
||||||
return orchestrator.NewService(logger, repo, opts...), nil
|
return orchestrator.NewService(logger, repo, opts...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,3 +327,19 @@ func buildCardGatewayRoutes(src map[string]cardGatewayRouteConfig) map[string]or
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildFeeLedgerAccounts(src map[string]string) map[string]string {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make(map[string]string, len(src))
|
||||||
|
for key, account := range src {
|
||||||
|
k := strings.ToLower(strings.TrimSpace(key))
|
||||||
|
v := strings.TrimSpace(account)
|
||||||
|
if k == "" || v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ func (f *paymentCommandFactory) QuotePayment() *quotePaymentCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *paymentCommandFactory) QuotePayments() *quotePaymentsCommand {
|
||||||
|
return "ePaymentsCommand{
|
||||||
|
engine: f.engine,
|
||||||
|
logger: f.logger.Named("quote_payments"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (f *paymentCommandFactory) InitiatePayment() *initiatePaymentCommand {
|
func (f *paymentCommandFactory) InitiatePayment() *initiatePaymentCommand {
|
||||||
return &initiatePaymentCommand{
|
return &initiatePaymentCommand{
|
||||||
engine: f.engine,
|
engine: f.engine,
|
||||||
@@ -68,6 +75,13 @@ func (f *paymentCommandFactory) InitiatePayment() *initiatePaymentCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *paymentCommandFactory) InitiatePayments() *initiatePaymentsCommand {
|
||||||
|
return &initiatePaymentsCommand{
|
||||||
|
engine: f.engine,
|
||||||
|
logger: f.logger.Named("initiate_payments"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (f *paymentCommandFactory) CancelPayment() *cancelPaymentCommand {
|
func (f *paymentCommandFactory) CancelPayment() *cancelPaymentCommand {
|
||||||
return &cancelPaymentCommand{
|
return &cancelPaymentCommand{
|
||||||
engine: f.engine,
|
engine: f.engine,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func intentFromProto(src *orchestratorv1.PaymentIntent) model.PaymentIntent {
|
|||||||
Amount: cloneMoney(src.GetAmount()),
|
Amount: cloneMoney(src.GetAmount()),
|
||||||
RequiresFX: src.GetRequiresFx(),
|
RequiresFX: src.GetRequiresFx(),
|
||||||
FeePolicy: src.GetFeePolicy(),
|
FeePolicy: src.GetFeePolicy(),
|
||||||
|
SettlementMode: src.GetSettlementMode(),
|
||||||
Attributes: cloneMetadata(src.GetAttributes()),
|
Attributes: cloneMetadata(src.GetAttributes()),
|
||||||
}
|
}
|
||||||
if src.GetFx() != nil {
|
if src.GetFx() != nil {
|
||||||
@@ -159,6 +160,7 @@ func protoIntentFromModel(src model.PaymentIntent) *orchestratorv1.PaymentIntent
|
|||||||
Amount: cloneMoney(src.Amount),
|
Amount: cloneMoney(src.Amount),
|
||||||
RequiresFx: src.RequiresFX,
|
RequiresFx: src.RequiresFX,
|
||||||
FeePolicy: src.FeePolicy,
|
FeePolicy: src.FeePolicy,
|
||||||
|
SettlementMode: src.SettlementMode,
|
||||||
Attributes: cloneMetadata(src.Attributes),
|
Attributes: cloneMetadata(src.Attributes),
|
||||||
}
|
}
|
||||||
if src.FX != nil {
|
if src.FX != nil {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/payments/orchestrator/storage"
|
"github.com/tech/sendico/payments/orchestrator/storage"
|
||||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||||
@@ -66,6 +67,177 @@ func (h *quotePaymentCommand) Execute(ctx context.Context, req *orchestratorv1.Q
|
|||||||
return gsresponse.Success(&orchestratorv1.QuotePaymentResponse{Quote: quote})
|
return gsresponse.Success(&orchestratorv1.QuotePaymentResponse{Quote: quote})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type quotePaymentsCommand struct {
|
||||||
|
engine paymentEngine
|
||||||
|
logger mlogger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentsCommand) Execute(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) gsresponse.Responder[orchestratorv1.QuotePaymentsResponse] {
|
||||||
|
if err := h.engine.EnsureRepository(ctx); err != nil {
|
||||||
|
return gsresponse.Unavailable[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
|
||||||
|
}
|
||||||
|
orgRef, orgID, err := validateMetaAndOrgRef(req.GetMeta())
|
||||||
|
if err != nil {
|
||||||
|
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
intents := req.GetIntents()
|
||||||
|
if len(intents) == 0 {
|
||||||
|
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("intents are required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
baseKey := strings.TrimSpace(req.GetIdempotencyKey())
|
||||||
|
quotes := make([]*orchestratorv1.PaymentQuote, 0, len(intents))
|
||||||
|
expires := make([]time.Time, 0, len(intents))
|
||||||
|
for i, intent := range intents {
|
||||||
|
if err := requireNonNilIntent(intent); err != nil {
|
||||||
|
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
quoteReq := &orchestratorv1.QuotePaymentRequest{
|
||||||
|
Meta: req.GetMeta(),
|
||||||
|
IdempotencyKey: perIntentIdempotencyKey(baseKey, i, len(intents)),
|
||||||
|
Intent: intent,
|
||||||
|
PreviewOnly: req.GetPreviewOnly(),
|
||||||
|
}
|
||||||
|
quote, expiresAt, err := h.engine.BuildPaymentQuote(ctx, orgRef, quoteReq)
|
||||||
|
if err != nil {
|
||||||
|
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
quotes = append(quotes, quote)
|
||||||
|
expires = append(expires, expiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregate, err := aggregatePaymentQuotes(quotes)
|
||||||
|
if err != nil {
|
||||||
|
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InternalWrap(err, "quote aggregation failed"))
|
||||||
|
}
|
||||||
|
expiresAt, ok := minQuoteExpiry(expires)
|
||||||
|
if !ok {
|
||||||
|
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.Internal("quote expiry missing"))
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteRef := ""
|
||||||
|
if !req.GetPreviewOnly() {
|
||||||
|
quotesStore, err := ensureQuotesStore(h.engine.Repository())
|
||||||
|
if err != nil {
|
||||||
|
return gsresponse.Unavailable[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
quoteRef = primitive.NewObjectID().Hex()
|
||||||
|
record := &model.PaymentQuoteRecord{
|
||||||
|
QuoteRef: quoteRef,
|
||||||
|
Intents: intentsFromProto(intents),
|
||||||
|
Quotes: quoteSnapshotsFromProto(quotes),
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
}
|
||||||
|
record.SetID(primitive.NewObjectID())
|
||||||
|
record.SetOrganizationRef(orgID)
|
||||||
|
if err := quotesStore.Create(ctx, record); err != nil {
|
||||||
|
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
h.logger.Info("stored payment quotes", zap.String("quote_ref", quoteRef), zap.String("org_ref", orgID.Hex()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return gsresponse.Success(&orchestratorv1.QuotePaymentsResponse{
|
||||||
|
QuoteRef: quoteRef,
|
||||||
|
Aggregate: aggregate,
|
||||||
|
Quotes: quotes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type initiatePaymentsCommand struct {
|
||||||
|
engine paymentEngine
|
||||||
|
logger mlogger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *initiatePaymentsCommand) Execute(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) gsresponse.Responder[orchestratorv1.InitiatePaymentsResponse] {
|
||||||
|
if err := h.engine.EnsureRepository(ctx); err != nil {
|
||||||
|
return gsresponse.Unavailable[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
|
||||||
|
}
|
||||||
|
_, orgID, err := validateMetaAndOrgRef(req.GetMeta())
|
||||||
|
if err != nil {
|
||||||
|
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
idempotencyKey, err := requireIdempotencyKey(req.GetIdempotencyKey())
|
||||||
|
if err != nil {
|
||||||
|
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
quoteRef := strings.TrimSpace(req.GetQuoteRef())
|
||||||
|
if quoteRef == "" {
|
||||||
|
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("quote_ref is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
quotesStore, err := ensureQuotesStore(h.engine.Repository())
|
||||||
|
if err != nil {
|
||||||
|
return gsresponse.Unavailable[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
record, err := quotesStore.GetByRef(ctx, orgID, quoteRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, storage.ErrQuoteNotFound) {
|
||||||
|
return gsresponse.FailedPrecondition[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, "quote_not_found", merrors.InvalidArgument("quote_ref not found or expired"))
|
||||||
|
}
|
||||||
|
return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
intents := record.Intents
|
||||||
|
quotes := record.Quotes
|
||||||
|
if len(intents) == 0 && record.Intent.Kind != "" && record.Intent.Kind != model.PaymentKindUnspecified {
|
||||||
|
intents = []model.PaymentIntent{record.Intent}
|
||||||
|
}
|
||||||
|
if len(quotes) == 0 && record.Quote != nil {
|
||||||
|
quotes = []*model.PaymentQuoteSnapshot{record.Quote}
|
||||||
|
}
|
||||||
|
if len(intents) == 0 || len(quotes) == 0 || len(intents) != len(quotes) {
|
||||||
|
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("stored quote payload is incomplete"))
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err := ensurePaymentsStore(h.engine.Repository())
|
||||||
|
if err != nil {
|
||||||
|
return gsresponse.Unavailable[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payments := make([]*orchestratorv1.Payment, 0, len(intents))
|
||||||
|
for i := range intents {
|
||||||
|
intentProto := protoIntentFromModel(intents[i])
|
||||||
|
if err := requireNonNilIntent(intentProto); err != nil {
|
||||||
|
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
quoteProto := modelQuoteToProto(quotes[i])
|
||||||
|
if quoteProto == nil {
|
||||||
|
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("stored quote is empty"))
|
||||||
|
}
|
||||||
|
quoteProto.QuoteRef = quoteRef
|
||||||
|
|
||||||
|
perKey := perIntentIdempotencyKey(idempotencyKey, i, len(intents))
|
||||||
|
if existing, err := getPaymentByIdempotencyKey(ctx, store, orgID, perKey); err == nil && existing != nil {
|
||||||
|
payments = append(payments, toProtoPayment(existing))
|
||||||
|
continue
|
||||||
|
} else if err != nil && !errors.Is(err, storage.ErrPaymentNotFound) {
|
||||||
|
return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entity := newPayment(orgID, intentProto, perKey, req.GetMetadata(), quoteProto)
|
||||||
|
if err = store.Create(ctx, entity); err != nil {
|
||||||
|
if errors.Is(err, storage.ErrDuplicatePayment) {
|
||||||
|
return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.DataConflict("payment already exists"))
|
||||||
|
}
|
||||||
|
return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.engine.ExecutePayment(ctx, store, entity, quoteProto); err != nil {
|
||||||
|
return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payments = append(payments, toProtoPayment(entity))
|
||||||
|
}
|
||||||
|
|
||||||
|
return gsresponse.Success(&orchestratorv1.InitiatePaymentsResponse{Payments: payments})
|
||||||
|
}
|
||||||
|
|
||||||
type initiatePaymentCommand struct {
|
type initiatePaymentCommand struct {
|
||||||
engine paymentEngine
|
engine paymentEngine
|
||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ func resolveTradeAmounts(intentAmount *moneyv1.Money, fxQuote *oraclev1.Quote, s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse, fxQuote *oraclev1.Quote) (*moneyv1.Money, *moneyv1.Money) {
|
func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse, fxQuote *oraclev1.Quote, mode orchestratorv1.SettlementMode) (*moneyv1.Money, *moneyv1.Money) {
|
||||||
if pay == nil {
|
if pay == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@ func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.Est
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adjustDebit := func(m *moneyv1.Money) {
|
applyChargeToDebit := func(m *moneyv1.Money) {
|
||||||
converted, err := ensureCurrency(m, pay.GetCurrency(), fxQuote)
|
converted, err := ensureCurrency(m, pay.GetCurrency(), fxQuote)
|
||||||
if err != nil || converted == nil {
|
if err != nil || converted == nil {
|
||||||
return
|
return
|
||||||
@@ -176,7 +176,7 @@ func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.Est
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adjustSettlement := func(m *moneyv1.Money) {
|
applyChargeToSettlement := func(m *moneyv1.Money) {
|
||||||
converted, err := ensureCurrency(m, settlementCurrency, fxQuote)
|
converted, err := ensureCurrency(m, settlementCurrency, fxQuote)
|
||||||
if err != nil || converted == nil {
|
if err != nil || converted == nil {
|
||||||
return
|
return
|
||||||
@@ -186,12 +186,22 @@ func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.Est
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adjustDebit(fee)
|
switch mode {
|
||||||
adjustSettlement(fee)
|
case orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_RECEIVED:
|
||||||
|
// Sender pays the fee: keep settlement fixed, increase debit.
|
||||||
|
applyChargeToDebit(fee)
|
||||||
|
default:
|
||||||
|
// Recipient pays the fee (default): reduce settlement, keep debit fixed.
|
||||||
|
applyChargeToSettlement(fee)
|
||||||
|
}
|
||||||
|
|
||||||
if network != nil && network.GetNetworkFee() != nil {
|
if network != nil && network.GetNetworkFee() != nil {
|
||||||
adjustDebit(network.GetNetworkFee())
|
switch mode {
|
||||||
adjustSettlement(network.GetNetworkFee())
|
case orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_RECEIVED:
|
||||||
|
applyChargeToDebit(network.GetNetworkFee())
|
||||||
|
default:
|
||||||
|
applyChargeToSettlement(network.GetNetworkFee())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return makeMoney(pay.GetCurrency(), debitDecimal), makeMoney(settlementCurrency, settlementDecimal)
|
return makeMoney(pay.GetCurrency(), debitDecimal), makeMoney(settlementCurrency, settlementDecimal)
|
||||||
@@ -383,6 +393,22 @@ func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*chainv1.Servic
|
|||||||
return breakdown
|
return breakdown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assignLedgerAccounts(lines []*feesv1.DerivedPostingLine, account string) []*feesv1.DerivedPostingLine {
|
||||||
|
if account == "" || len(lines) == 0 {
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
for _, line := range lines {
|
||||||
|
if line == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(line.GetLedgerAccountRef()) != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line.LedgerAccountRef = account
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
func moneyEquals(a, b *moneyv1.Money) bool {
|
func moneyEquals(a, b *moneyv1.Money) bool {
|
||||||
if a == nil || b == nil {
|
if a == nil || b == nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestResolveTradeAmountsBuyBase(t *testing.T) {
|
func TestResolveTradeAmountsBuyBase(t *testing.T) {
|
||||||
@@ -47,11 +48,32 @@ func TestComputeAggregatesConvertsCurrencies(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
debit, settlement := computeAggregates(pay, settle, fee, network, fxQuote)
|
debit, settlement := computeAggregates(pay, settle, fee, network, fxQuote, orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_RECEIVED)
|
||||||
if debit.GetCurrency() != "USD" || debit.GetAmount() != "115" {
|
if debit.GetCurrency() != "USD" || debit.GetAmount() != "115" {
|
||||||
t.Fatalf("expected debit 115 USD, got %s %s", debit.GetCurrency(), debit.GetAmount())
|
t.Fatalf("expected debit 115 USD, got %s %s", debit.GetCurrency(), debit.GetAmount())
|
||||||
}
|
}
|
||||||
if settlement.GetCurrency() != "EUR" || settlement.GetAmount() != "42.5" {
|
if settlement.GetCurrency() != "EUR" || settlement.GetAmount() != "50" {
|
||||||
t.Fatalf("expected settlement 42.5 EUR, got %s %s", settlement.GetCurrency(), settlement.GetAmount())
|
t.Fatalf("expected settlement 50 EUR, got %s %s", settlement.GetCurrency(), settlement.GetAmount())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeAggregatesRecipientPaysFee(t *testing.T) {
|
||||||
|
pay := &moneyv1.Money{Currency: "USDT", Amount: "100"}
|
||||||
|
settle := &moneyv1.Money{Currency: "RUB", Amount: "7932"} // 100 * 79.32
|
||||||
|
fee := &moneyv1.Money{Currency: "USDT", Amount: "7"} // 7% of 100
|
||||||
|
fxQuote := &oraclev1.Quote{
|
||||||
|
Pair: &fxv1.CurrencyPair{Base: "USDT", Quote: "RUB"},
|
||||||
|
Side: fxv1.Side_SELL_BASE_BUY_QUOTE,
|
||||||
|
Price: &moneyv1.Decimal{
|
||||||
|
Value: "79.32",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
debit, settlement := computeAggregates(pay, settle, fee, nil, fxQuote, orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_SOURCE)
|
||||||
|
if debit.GetCurrency() != "USDT" || debit.GetAmount() != "100" {
|
||||||
|
t.Fatalf("expected debit 100 USDT, got %s %s", debit.GetCurrency(), debit.GetAmount())
|
||||||
|
}
|
||||||
|
if settlement.GetCurrency() != "RUB" || settlement.GetAmount() != "7376.76" {
|
||||||
|
t.Fatalf("expected settlement 7376.76 RUB, got %s %s", settlement.GetCurrency(), settlement.GetAmount())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,24 @@ func WithCardGatewayRoutes(routes map[string]CardGatewayRoute) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithFeeLedgerAccounts maps gateway identifiers to ledger accounts used for fees.
|
||||||
|
func WithFeeLedgerAccounts(routes map[string]string) Option {
|
||||||
|
return func(s *Service) {
|
||||||
|
if len(routes) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.deps.feeLedgerAccounts = make(map[string]string, len(routes))
|
||||||
|
for k, v := range routes {
|
||||||
|
key := strings.ToLower(strings.TrimSpace(k))
|
||||||
|
val := strings.TrimSpace(v)
|
||||||
|
if key == "" || val == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.deps.feeLedgerAccounts[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithClock overrides the default clock.
|
// WithClock overrides the default clock.
|
||||||
func WithClock(clock clockpkg.Clock) Option {
|
func WithClock(clock clockpkg.Clock) Option {
|
||||||
return func(s *Service) {
|
return func(s *Service) {
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package orchestrator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func perIntentIdempotencyKey(base string, index int, total int) string {
|
||||||
|
base = strings.TrimSpace(base)
|
||||||
|
if base == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if total <= 1 {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s:%d", base, index+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func minQuoteExpiry(expires []time.Time) (time.Time, bool) {
|
||||||
|
var min time.Time
|
||||||
|
for _, exp := range expires {
|
||||||
|
if exp.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if min.IsZero() || exp.Before(min) {
|
||||||
|
min = exp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if min.IsZero() {
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
return min, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func aggregatePaymentQuotes(quotes []*orchestratorv1.PaymentQuote) (*orchestratorv1.PaymentQuoteAggregate, error) {
|
||||||
|
if len(quotes) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
debitTotals := map[string]decimal.Decimal{}
|
||||||
|
settlementTotals := map[string]decimal.Decimal{}
|
||||||
|
feeTotals := map[string]decimal.Decimal{}
|
||||||
|
networkTotals := map[string]decimal.Decimal{}
|
||||||
|
|
||||||
|
for _, quote := range quotes {
|
||||||
|
if quote == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := accumulateMoney(debitTotals, quote.GetDebitAmount()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := accumulateMoney(settlementTotals, quote.GetExpectedSettlementAmount()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := accumulateMoney(feeTotals, quote.GetExpectedFeeTotal()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if nf := quote.GetNetworkFee(); nf != nil {
|
||||||
|
if err := accumulateMoney(networkTotals, nf.GetNetworkFee()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &orchestratorv1.PaymentQuoteAggregate{
|
||||||
|
DebitAmounts: totalsToMoney(debitTotals),
|
||||||
|
ExpectedSettlementAmounts: totalsToMoney(settlementTotals),
|
||||||
|
ExpectedFeeTotals: totalsToMoney(feeTotals),
|
||||||
|
NetworkFeeTotals: totalsToMoney(networkTotals),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func accumulateMoney(totals map[string]decimal.Decimal, money *moneyv1.Money) error {
|
||||||
|
if money == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
currency := strings.TrimSpace(money.GetCurrency())
|
||||||
|
if currency == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
amount, err := decimal.NewFromString(money.GetAmount())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if current, ok := totals[currency]; ok {
|
||||||
|
totals[currency] = current.Add(amount)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
totals[currency] = amount
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func totalsToMoney(totals map[string]decimal.Decimal) []*moneyv1.Money {
|
||||||
|
if len(totals) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
currencies := make([]string, 0, len(totals))
|
||||||
|
for currency := range totals {
|
||||||
|
currencies = append(currencies, currency)
|
||||||
|
}
|
||||||
|
sort.Strings(currencies)
|
||||||
|
|
||||||
|
result := make([]*moneyv1.Money, 0, len(currencies))
|
||||||
|
for _, currency := range currencies {
|
||||||
|
amount := totals[currency]
|
||||||
|
result = append(result, &moneyv1.Money{
|
||||||
|
Amount: amount.String(),
|
||||||
|
Currency: currency,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func intentsFromProto(intents []*orchestratorv1.PaymentIntent) []model.PaymentIntent {
|
||||||
|
if len(intents) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]model.PaymentIntent, 0, len(intents))
|
||||||
|
for _, intent := range intents {
|
||||||
|
result = append(result, intentFromProto(intent))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func quoteSnapshotsFromProto(quotes []*orchestratorv1.PaymentQuote) []*model.PaymentQuoteSnapshot {
|
||||||
|
if len(quotes) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]*model.PaymentQuoteSnapshot, 0, len(quotes))
|
||||||
|
for _, quote := range quotes {
|
||||||
|
if quote == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if snapshot := quoteSnapshotToModel(quote); snapshot != nil {
|
||||||
|
result = append(result, snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package orchestrator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAggregatePaymentQuotes(t *testing.T) {
|
||||||
|
quotes := []*orchestratorv1.PaymentQuote{
|
||||||
|
{
|
||||||
|
DebitAmount: &moneyv1.Money{Amount: "10", Currency: "USD"},
|
||||||
|
ExpectedSettlementAmount: &moneyv1.Money{Amount: "8", Currency: "EUR"},
|
||||||
|
ExpectedFeeTotal: &moneyv1.Money{Amount: "1", Currency: "USD"},
|
||||||
|
NetworkFee: &chainv1.EstimateTransferFeeResponse{
|
||||||
|
NetworkFee: &moneyv1.Money{Amount: "0.5", Currency: "USD"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DebitAmount: &moneyv1.Money{Amount: "5", Currency: "USD"},
|
||||||
|
ExpectedSettlementAmount: &moneyv1.Money{Amount: "1000", Currency: "NGN"},
|
||||||
|
ExpectedFeeTotal: &moneyv1.Money{Amount: "2", Currency: "USD"},
|
||||||
|
NetworkFee: &chainv1.EstimateTransferFeeResponse{
|
||||||
|
NetworkFee: &moneyv1.Money{Amount: "100", Currency: "NGN"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
agg, err := aggregatePaymentQuotes(quotes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("aggregatePaymentQuotes returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertMoneyTotals(t, agg.GetDebitAmounts(), map[string]string{"USD": "15"})
|
||||||
|
assertMoneyTotals(t, agg.GetExpectedSettlementAmounts(), map[string]string{"EUR": "8", "NGN": "1000"})
|
||||||
|
assertMoneyTotals(t, agg.GetExpectedFeeTotals(), map[string]string{"USD": "3"})
|
||||||
|
assertMoneyTotals(t, agg.GetNetworkFeeTotals(), map[string]string{"USD": "0.5", "NGN": "100"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregatePaymentQuotesInvalidAmount(t *testing.T) {
|
||||||
|
quotes := []*orchestratorv1.PaymentQuote{
|
||||||
|
{
|
||||||
|
DebitAmount: &moneyv1.Money{Amount: "bad", Currency: "USD"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, err := aggregatePaymentQuotes(quotes); err == nil {
|
||||||
|
t.Fatal("expected error for invalid amount")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMinQuoteExpiry(t *testing.T) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
later := now.Add(10 * time.Minute)
|
||||||
|
earliest := now.Add(5 * time.Minute)
|
||||||
|
|
||||||
|
min, ok := minQuoteExpiry([]time.Time{later, time.Time{}, earliest})
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected min expiry to be set")
|
||||||
|
}
|
||||||
|
if !min.Equal(earliest) {
|
||||||
|
t.Fatalf("expected min expiry %v, got %v", earliest, min)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := minQuoteExpiry([]time.Time{time.Time{}}); ok {
|
||||||
|
t.Fatal("expected min expiry to be unset")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertMoneyTotals(t *testing.T, list []*moneyv1.Money, expected map[string]string) {
|
||||||
|
t.Helper()
|
||||||
|
got := make(map[string]decimal.Decimal, len(list))
|
||||||
|
for _, item := range list {
|
||||||
|
if item == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val, err := decimal.NewFromString(item.GetAmount())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("invalid money amount %q: %v", item.GetAmount(), err)
|
||||||
|
}
|
||||||
|
got[item.GetCurrency()] = val
|
||||||
|
}
|
||||||
|
if len(got) != len(expected) {
|
||||||
|
t.Fatalf("expected %d totals, got %d", len(expected), len(got))
|
||||||
|
}
|
||||||
|
for currency, amount := range expected {
|
||||||
|
val, err := decimal.NewFromString(amount)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("invalid expected amount %q: %v", amount, err)
|
||||||
|
}
|
||||||
|
gotVal, ok := got[currency]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("missing currency %s", currency)
|
||||||
|
}
|
||||||
|
if !gotVal.Equal(val) {
|
||||||
|
t.Fatalf("expected %s %s, got %s", amount, currency, gotVal.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,9 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
|
|||||||
} else if amount != nil {
|
} else if amount != nil {
|
||||||
feeCurrency = amount.GetCurrency()
|
feeCurrency = amount.GetCurrency()
|
||||||
}
|
}
|
||||||
feeTotal := extractFeeTotal(feeQuote.GetLines(), feeCurrency)
|
feeLines := cloneFeeLines(feeQuote.GetLines())
|
||||||
|
s.assignFeeLedgerAccounts(intent, feeLines)
|
||||||
|
feeTotal := extractFeeTotal(feeLines, feeCurrency)
|
||||||
|
|
||||||
var networkFee *chainv1.EstimateTransferFeeResponse
|
var networkFee *chainv1.EstimateTransferFeeResponse
|
||||||
if shouldEstimateNetworkFee(intent) {
|
if shouldEstimateNetworkFee(intent) {
|
||||||
@@ -63,13 +65,13 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
|
|||||||
s.logger.Debug("network fee estimated", zap.String("org_ref", orgRef))
|
s.logger.Debug("network fee estimated", zap.String("org_ref", orgRef))
|
||||||
}
|
}
|
||||||
|
|
||||||
debitAmount, settlementAmount := computeAggregates(payAmount, settlementAmountBeforeFees, feeTotal, networkFee, fxQuote)
|
debitAmount, settlementAmount := computeAggregates(payAmount, settlementAmountBeforeFees, feeTotal, networkFee, fxQuote, intent.GetSettlementMode())
|
||||||
|
|
||||||
quote := &orchestratorv1.PaymentQuote{
|
quote := &orchestratorv1.PaymentQuote{
|
||||||
DebitAmount: debitAmount,
|
DebitAmount: debitAmount,
|
||||||
ExpectedSettlementAmount: settlementAmount,
|
ExpectedSettlementAmount: settlementAmount,
|
||||||
ExpectedFeeTotal: feeTotal,
|
ExpectedFeeTotal: feeTotal,
|
||||||
FeeLines: cloneFeeLines(feeQuote.GetLines()),
|
FeeLines: feeLines,
|
||||||
FeeRules: cloneFeeRules(feeQuote.GetApplied()),
|
FeeRules: cloneFeeRules(feeQuote.GetApplied()),
|
||||||
FxQuote: fxQuote,
|
FxQuote: fxQuote,
|
||||||
NetworkFee: networkFee,
|
NetworkFee: networkFee,
|
||||||
@@ -207,3 +209,53 @@ func (s *Service) requestFXQuote(ctx context.Context, orgRef string, req *orches
|
|||||||
}
|
}
|
||||||
return quoteToProto(quote), nil
|
return quoteToProto(quote), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) feeLedgerAccountForIntent(intent *orchestratorv1.PaymentIntent) string {
|
||||||
|
if intent == nil || len(s.deps.feeLedgerAccounts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
key := s.gatewayKeyFromIntent(intent)
|
||||||
|
if key == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(s.deps.feeLedgerAccounts[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) assignFeeLedgerAccounts(intent *orchestratorv1.PaymentIntent, lines []*feesv1.DerivedPostingLine) {
|
||||||
|
account := s.feeLedgerAccountForIntent(intent)
|
||||||
|
key := s.gatewayKeyFromIntent(intent)
|
||||||
|
|
||||||
|
missing := 0
|
||||||
|
for _, line := range lines {
|
||||||
|
if line == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(line.GetLedgerAccountRef()) == "" {
|
||||||
|
missing++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if missing == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if account == "" {
|
||||||
|
s.logger.Debug("no fee ledger account mapping found", zap.String("gateway", key), zap.Int("missing_lines", missing))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assignLedgerAccounts(lines, account)
|
||||||
|
s.logger.Debug("applied fee ledger account mapping", zap.String("gateway", key), zap.String("ledger_account", account), zap.Int("lines", missing))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) gatewayKeyFromIntent(intent *orchestratorv1.PaymentIntent) string {
|
||||||
|
if intent == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(intent.GetAttributes()["gateway"])
|
||||||
|
if key == "" {
|
||||||
|
if dest := intent.GetDestination(); dest != nil && dest.GetCard() != nil {
|
||||||
|
key = defaultCardGateway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.ToLower(key)
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type serviceDependencies struct {
|
|||||||
oracle oracleDependency
|
oracle oracleDependency
|
||||||
mntx mntxDependency
|
mntx mntxDependency
|
||||||
cardRoutes map[string]CardGatewayRoute
|
cardRoutes map[string]CardGatewayRoute
|
||||||
|
feeLedgerAccounts map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type handlerSet struct {
|
type handlerSet struct {
|
||||||
@@ -116,12 +117,24 @@ func (s *Service) QuotePayment(ctx context.Context, req *orchestratorv1.QuotePay
|
|||||||
return executeUnary(ctx, s, "QuotePayment", s.h.commands.QuotePayment().Execute, req)
|
return executeUnary(ctx, s, "QuotePayment", s.h.commands.QuotePayment().Execute, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QuotePayments aggregates downstream quotes for multiple intents.
|
||||||
|
func (s *Service) QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error) {
|
||||||
|
s.ensureHandlers()
|
||||||
|
return executeUnary(ctx, s, "QuotePayments", s.h.commands.QuotePayments().Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
// InitiatePayment captures a payment intent and reserves funds orchestration.
|
// InitiatePayment captures a payment intent and reserves funds orchestration.
|
||||||
func (s *Service) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) {
|
func (s *Service) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) {
|
||||||
s.ensureHandlers()
|
s.ensureHandlers()
|
||||||
return executeUnary(ctx, s, "InitiatePayment", s.h.commands.InitiatePayment().Execute, req)
|
return executeUnary(ctx, s, "InitiatePayment", s.h.commands.InitiatePayment().Execute, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitiatePayments executes multiple payments using a stored quote reference.
|
||||||
|
func (s *Service) InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error) {
|
||||||
|
s.ensureHandlers()
|
||||||
|
return executeUnary(ctx, s, "InitiatePayments", s.h.commands.InitiatePayments().Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
// CancelPayment attempts to cancel an in-flight payment.
|
// CancelPayment attempts to cancel an in-flight payment.
|
||||||
func (s *Service) CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error) {
|
func (s *Service) CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error) {
|
||||||
s.ensureHandlers()
|
s.ensureHandlers()
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ func TestNewPayment(t *testing.T) {
|
|||||||
org := primitive.NewObjectID()
|
org := primitive.NewObjectID()
|
||||||
intent := &orchestratorv1.PaymentIntent{
|
intent := &orchestratorv1.PaymentIntent{
|
||||||
Amount: &moneyv1.Money{Currency: "USD", Amount: "10"},
|
Amount: &moneyv1.Money{Currency: "USD", Amount: "10"},
|
||||||
|
SettlementMode: orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_RECEIVED,
|
||||||
}
|
}
|
||||||
quote := &orchestratorv1.PaymentQuote{QuoteRef: "q1"}
|
quote := &orchestratorv1.PaymentQuote{QuoteRef: "q1"}
|
||||||
p := newPayment(org, intent, "idem", map[string]string{"k": "v"}, quote)
|
p := newPayment(org, intent, "idem", map[string]string{"k": "v"}, quote)
|
||||||
@@ -58,6 +59,9 @@ func TestNewPayment(t *testing.T) {
|
|||||||
if p.Intent.Amount == nil || p.Intent.Amount.GetAmount() != "10" {
|
if p.Intent.Amount == nil || p.Intent.Amount.GetAmount() != "10" {
|
||||||
t.Fatalf("intent not copied")
|
t.Fatalf("intent not copied")
|
||||||
}
|
}
|
||||||
|
if p.Intent.SettlementMode != orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_RECEIVED {
|
||||||
|
t.Fatalf("settlement mode not preserved")
|
||||||
|
}
|
||||||
if p.LastQuote == nil || p.LastQuote.QuoteRef != "q1" {
|
if p.LastQuote == nil || p.LastQuote.QuoteRef != "q1" {
|
||||||
t.Fatalf("quote not copied")
|
t.Fatalf("quote not copied")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PaymentKind captures the orchestrator intent type.
|
// PaymentKind captures the orchestrator intent type.
|
||||||
@@ -131,6 +132,7 @@ type PaymentIntent struct {
|
|||||||
RequiresFX bool `bson:"requiresFx,omitempty" json:"requiresFx,omitempty"`
|
RequiresFX bool `bson:"requiresFx,omitempty" json:"requiresFx,omitempty"`
|
||||||
FX *FXIntent `bson:"fx,omitempty" json:"fx,omitempty"`
|
FX *FXIntent `bson:"fx,omitempty" json:"fx,omitempty"`
|
||||||
FeePolicy *feesv1.PolicyOverrides `bson:"feePolicy,omitempty" json:"feePolicy,omitempty"`
|
FeePolicy *feesv1.PolicyOverrides `bson:"feePolicy,omitempty" json:"feePolicy,omitempty"`
|
||||||
|
SettlementMode orchestratorv1.SettlementMode `bson:"settlementMode,omitempty" json:"settlementMode,omitempty"`
|
||||||
Attributes map[string]string `bson:"attributes,omitempty" json:"attributes,omitempty"`
|
Attributes map[string]string `bson:"attributes,omitempty" json:"attributes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ type PaymentQuoteRecord struct {
|
|||||||
model.OrganizationBoundBase `bson:",inline" json:",inline"`
|
model.OrganizationBoundBase `bson:",inline" json:",inline"`
|
||||||
|
|
||||||
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
|
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
|
||||||
Intent PaymentIntent `bson:"intent" json:"intent"`
|
Intent PaymentIntent `bson:"intent,omitempty" json:"intent,omitempty"`
|
||||||
Quote *PaymentQuoteSnapshot `bson:"quote" json:"quote"`
|
Intents []PaymentIntent `bson:"intents,omitempty" json:"intents,omitempty"`
|
||||||
|
Quote *PaymentQuoteSnapshot `bson:"quote,omitempty" json:"quote,omitempty"`
|
||||||
|
Quotes []*PaymentQuoteSnapshot `bson:"quotes,omitempty" json:"quotes,omitempty"`
|
||||||
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
|
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,16 @@ func (q *Quotes) Create(ctx context.Context, quote *model.PaymentQuoteRecord) er
|
|||||||
quote.Intent.Attributes[k] = strings.TrimSpace(v)
|
quote.Intent.Attributes[k] = strings.TrimSpace(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(quote.Intents) > 0 {
|
||||||
|
for i := range quote.Intents {
|
||||||
|
if quote.Intents[i].Attributes == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for k, v := range quote.Intents[i].Attributes {
|
||||||
|
quote.Intents[i].Attributes[k] = strings.TrimSpace(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
quote.Update()
|
quote.Update()
|
||||||
|
|
||||||
filter := repository.OrgFilter(quote.OrganizationRef).And(
|
filter := repository.OrgFilter(quote.OrganizationRef).And(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/mattn/go-colorable v0.1.14
|
github.com/mattn/go-colorable v0.1.14
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/nats-io/nats.go v1.47.0
|
github.com/nats-io/nats.go v1.48.0
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/testcontainers/testcontainers-go v0.33.0
|
github.com/testcontainers/testcontainers-go v0.33.0
|
||||||
@@ -18,7 +18,7 @@ require (
|
|||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
golang.org/x/crypto v0.46.0
|
golang.org/x/crypto v0.46.0
|
||||||
google.golang.org/grpc v1.77.0
|
google.golang.org/grpc v1.77.0
|
||||||
google.golang.org/protobuf v1.36.10
|
google.golang.org/protobuf v1.36.11
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -93,6 +93,6 @@ require (
|
|||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
@@ -269,12 +269,12 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
|||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CryptoAddressPaymentData struct {
|
type CryptoAddressPaymentData struct {
|
||||||
|
Currency Currency `bson:"currency" json:"currency"`
|
||||||
Address string `bson:"address" json:"address"`
|
Address string `bson:"address" json:"address"`
|
||||||
Network string `bson:"network" json:"network"`
|
Network string `bson:"network" json:"network"`
|
||||||
DestinationTag *string `bson:"destinationTag,omitempty" json:"destinationTag,omitempty"`
|
DestinationTag *string `bson:"destinationTag,omitempty" json:"destinationTag,omitempty"`
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const (
|
|||||||
CurrencyUAH Currency = "UAH" // Ukrainian Hryvnia
|
CurrencyUAH Currency = "UAH" // Ukrainian Hryvnia
|
||||||
CurrencyPLN Currency = "PLN" // Polish Złoty
|
CurrencyPLN Currency = "PLN" // Polish Złoty
|
||||||
CurrencyCZK Currency = "CZK" // Czech Koruna
|
CurrencyCZK Currency = "CZK" // Czech Koruna
|
||||||
|
CurrencyUSDT Currency = "USDT" // Czech Koruna
|
||||||
)
|
)
|
||||||
|
|
||||||
// All supported currencies
|
// All supported currencies
|
||||||
@@ -19,6 +20,7 @@ var SupportedCurrencies = []Currency{
|
|||||||
CurrencyUAH,
|
CurrencyUAH,
|
||||||
CurrencyPLN,
|
CurrencyPLN,
|
||||||
CurrencyCZK,
|
CurrencyCZK,
|
||||||
|
CurrencyUSDT,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Amount struct {
|
type Amount struct {
|
||||||
|
|||||||
@@ -20,17 +20,18 @@ message RateSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message RequestMeta {
|
message RequestMeta {
|
||||||
string request_ref = 1 [deprecated = true];
|
reserved 1, 4, 5;
|
||||||
|
reserved "request_ref", "idempotency_key", "trace_ref";
|
||||||
|
|
||||||
string tenant_ref = 2;
|
string tenant_ref = 2;
|
||||||
string organization_ref = 3;
|
string organization_ref = 3;
|
||||||
string idempotency_key = 4 [deprecated = true];
|
|
||||||
string trace_ref = 5 [deprecated = true];
|
|
||||||
common.trace.v1.TraceContext trace = 6;
|
common.trace.v1.TraceContext trace = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ResponseMeta {
|
message ResponseMeta {
|
||||||
string request_ref = 1 [deprecated = true];
|
reserved 1, 2;
|
||||||
string trace_ref = 2 [deprecated = true];
|
reserved "request_ref", "trace_ref";
|
||||||
|
|
||||||
common.trace.v1.TraceContext trace = 3;
|
common.trace.v1.TraceContext trace = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,13 @@ message PaymentQuote {
|
|||||||
string quote_ref = 8;
|
string quote_ref = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message PaymentQuoteAggregate {
|
||||||
|
repeated common.money.v1.Money debit_amounts = 1;
|
||||||
|
repeated common.money.v1.Money expected_settlement_amounts = 2;
|
||||||
|
repeated common.money.v1.Money expected_fee_totals = 3;
|
||||||
|
repeated common.money.v1.Money network_fee_totals = 4;
|
||||||
|
}
|
||||||
|
|
||||||
message ExecutionRefs {
|
message ExecutionRefs {
|
||||||
string debit_entry_ref = 1;
|
string debit_entry_ref = 1;
|
||||||
string credit_entry_ref = 2;
|
string credit_entry_ref = 2;
|
||||||
@@ -172,6 +179,30 @@ message QuotePaymentResponse {
|
|||||||
PaymentQuote quote = 1;
|
PaymentQuote quote = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message QuotePaymentsRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
string idempotency_key = 2;
|
||||||
|
repeated PaymentIntent intents = 3;
|
||||||
|
bool preview_only = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message QuotePaymentsResponse {
|
||||||
|
string quote_ref = 1;
|
||||||
|
PaymentQuoteAggregate aggregate = 2;
|
||||||
|
repeated PaymentQuote quotes = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InitiatePaymentsRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
string idempotency_key = 2;
|
||||||
|
string quote_ref = 3;
|
||||||
|
map<string, string> metadata = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InitiatePaymentsResponse {
|
||||||
|
repeated Payment payments = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message InitiatePaymentRequest {
|
message InitiatePaymentRequest {
|
||||||
RequestMeta meta = 1;
|
RequestMeta meta = 1;
|
||||||
string idempotency_key = 2;
|
string idempotency_key = 2;
|
||||||
@@ -259,6 +290,8 @@ message InitiateConversionResponse {
|
|||||||
|
|
||||||
service PaymentOrchestrator {
|
service PaymentOrchestrator {
|
||||||
rpc QuotePayment(QuotePaymentRequest) returns (QuotePaymentResponse);
|
rpc QuotePayment(QuotePaymentRequest) returns (QuotePaymentResponse);
|
||||||
|
rpc QuotePayments(QuotePaymentsRequest) returns (QuotePaymentsResponse);
|
||||||
|
rpc InitiatePayments(InitiatePaymentsRequest) returns (InitiatePaymentsResponse);
|
||||||
rpc InitiatePayment(InitiatePaymentRequest) returns (InitiatePaymentResponse);
|
rpc InitiatePayment(InitiatePaymentRequest) returns (InitiatePaymentResponse);
|
||||||
rpc CancelPayment(CancelPaymentRequest) returns (CancelPaymentResponse);
|
rpc CancelPayment(CancelPaymentRequest) returns (CancelPaymentResponse);
|
||||||
rpc GetPayment(GetPaymentRequest) returns (GetPaymentResponse);
|
rpc GetPayment(GetPaymentRequest) returns (GetPaymentResponse);
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ api:
|
|||||||
call_timeout_seconds: 5
|
call_timeout_seconds: 5
|
||||||
insecure: true
|
insecure: true
|
||||||
payment_orchestrator:
|
payment_orchestrator:
|
||||||
address: sendico_payment_orchestrator:50062
|
address: sendico_payments_orchestrator:50062
|
||||||
address_env: PAYMENT_ORCHESTRATOR_ADDRESS
|
address_env: PAYMENTS_ADDRESS
|
||||||
dial_timeout_seconds: 5
|
dial_timeout_seconds: 5
|
||||||
call_timeout_seconds: 5
|
call_timeout_seconds: 5
|
||||||
insecure: true
|
insecure: true
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ replace github.com/tech/sendico/gateway/chain => ../gateway/chain
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.0
|
github.com/aws/aws-sdk-go-v2 v1.41.0
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.5
|
github.com/aws/aws-sdk-go-v2/config v1.32.6
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.6
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/go-chi/jwtauth/v5 v5.3.3
|
github.com/go-chi/jwtauth/v5 v5.3.3
|
||||||
@@ -32,7 +32,7 @@ require (
|
|||||||
go.mongodb.org/mongo-driver v1.17.6
|
go.mongodb.org/mongo-driver v1.17.6
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
golang.org/x/net v0.48.0
|
golang.org/x/net v0.48.0
|
||||||
google.golang.org/protobuf v1.36.10
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
moul.io/chizap v1.0.3
|
moul.io/chizap v1.0.3
|
||||||
)
|
)
|
||||||
@@ -58,7 +58,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
||||||
github.com/aws/smithy-go v1.24.0 // indirect
|
github.com/aws/smithy-go v1.24.0 // indirect
|
||||||
@@ -103,7 +103,7 @@ require (
|
|||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.48.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
@@ -139,6 +139,6 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||||
google.golang.org/grpc v1.77.0 // indirect
|
google.golang.org/grpc v1.77.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgP
|
|||||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8=
|
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.5/go.mod h1:xmDjzSUs/d0BB7ClzYPAZMmgQdrodNjPPhd6bGASwoE=
|
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hgHYfQFIk1cHDXrR4=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||||
@@ -32,12 +32,12 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy
|
|||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2 h1:U3ygWUhCpiSPYSHOrRhb3gOl9T5Y3kB8k5Vjs//57bE=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||||
@@ -175,8 +175,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
@@ -361,12 +361,12 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
|||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|||||||
@@ -36,6 +36,27 @@ func (r *QuotePayment) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QuotePayments struct {
|
||||||
|
PaymentBase `json:",inline"`
|
||||||
|
Intents []PaymentIntent `json:"intents"`
|
||||||
|
PreviewOnly bool `json:"previewOnly"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *QuotePayments) Validate() error {
|
||||||
|
if err := r.PaymentBase.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(r.Intents) == 0 {
|
||||||
|
return merrors.InvalidArgument("intents are required", "intents")
|
||||||
|
}
|
||||||
|
for i := range r.Intents {
|
||||||
|
if err := r.Intents[i].Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type InitiatePayment struct {
|
type InitiatePayment struct {
|
||||||
PaymentBase `json:",inline"`
|
PaymentBase `json:",inline"`
|
||||||
Intent *PaymentIntent `json:"intent,omitempty"`
|
Intent *PaymentIntent `json:"intent,omitempty"`
|
||||||
@@ -68,3 +89,18 @@ func (r InitiatePayment) Validate() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InitiatePayments struct {
|
||||||
|
PaymentBase `json:",inline"`
|
||||||
|
QuoteRef string `json:"quoteRef,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r InitiatePayments) Validate() error {
|
||||||
|
if err := r.PaymentBase.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r.QuoteRef == "" {
|
||||||
|
return merrors.InvalidArgument("quoteRef is required", "quoteRef")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ type PaymentIntent struct {
|
|||||||
Attributes map[string]string `json:"attributes,omitempty"`
|
Attributes map[string]string `json:"attributes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AssetResolverStub struct{}
|
||||||
|
|
||||||
|
func (a *AssetResolverStub) IsSupported(_ string) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PaymentIntent) Validate() error {
|
func (p *PaymentIntent) Validate() error {
|
||||||
// Kind must be set (non-zero)
|
// Kind must be set (non-zero)
|
||||||
var zeroKind PaymentKind
|
var zeroKind PaymentKind
|
||||||
@@ -33,7 +39,8 @@ func (p *PaymentIntent) Validate() error {
|
|||||||
if p.Amount == nil {
|
if p.Amount == nil {
|
||||||
return merrors.InvalidArgument("amount is required", "intent.amount")
|
return merrors.InvalidArgument("amount is required", "intent.amount")
|
||||||
}
|
}
|
||||||
if err := ValidateMoney(p.Amount); err != nil {
|
//TODO: collect supported currencies and validate against them
|
||||||
|
if err := ValidateMoney(p.Amount, &AssetResolverStub{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,77 @@
|
|||||||
package srequest
|
package srequest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidateMoney(m *model.Money) error {
|
// AssetResolver defines environment-specific supported assets.
|
||||||
if m.Amount == "" {
|
// Implementations should check:
|
||||||
return merrors.InvalidArgument("amount is required", "intent.amount")
|
// - fiat assets (ISO-4217)
|
||||||
|
// - crypto assets supported by gateways / FX providers
|
||||||
|
type AssetResolver interface {
|
||||||
|
IsSupported(ticker string) bool
|
||||||
}
|
}
|
||||||
if m.Currency == "" {
|
|
||||||
|
// Precompile regex for efficiency.
|
||||||
|
var currencySyntax = regexp.MustCompile(`^[A-Z0-9]{2,10}$`)
|
||||||
|
|
||||||
|
// ValidateCurrency validates currency syntax and checks dictionary via assetResolver.
|
||||||
|
func ValidateCurrency(cur string, assetResolver AssetResolver) error {
|
||||||
|
// Basic presence
|
||||||
|
if strings.TrimSpace(cur) == "" {
|
||||||
return merrors.InvalidArgument("currency is required", "intent.currency")
|
return merrors.InvalidArgument("currency is required", "intent.currency")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := decimal.NewFromString(m.Amount); err != nil {
|
// Normalize
|
||||||
return merrors.InvalidArgument("invalid amount decimal", "intent.amount")
|
cur = strings.ToUpper(strings.TrimSpace(cur))
|
||||||
|
|
||||||
|
// Syntax check
|
||||||
|
if !currencySyntax.MatchString(cur) {
|
||||||
|
return merrors.InvalidArgument(
|
||||||
|
"invalid currency format (must be A–Z0–9, length 2–10)",
|
||||||
|
"intent.currency",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m.Currency) != 3 {
|
// Dictionary validation
|
||||||
return merrors.InvalidArgument("currency must be 3 letters", "intent.currency")
|
if assetResolver == nil {
|
||||||
|
return merrors.InvalidArgument("asset resolver is not configured", "intent.currency")
|
||||||
}
|
}
|
||||||
for _, c := range m.Currency {
|
|
||||||
if c < 'A' || c > 'Z' {
|
if !assetResolver.IsSupported(cur) {
|
||||||
return merrors.InvalidArgument("currency must be uppercase A-Z", "intent.currency")
|
return merrors.InvalidArgument("unsupported currency/asset", "intent.currency")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateMoney(m *model.Money, assetResolver AssetResolver) error {
|
||||||
|
if m == nil {
|
||||||
|
return merrors.InvalidArgument("money is required", "intent.amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Basic presence
|
||||||
|
if strings.TrimSpace(m.Amount) == "" {
|
||||||
|
return merrors.InvalidArgument("amount is required", "intent.amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Validate decimal amount
|
||||||
|
amount, err := decimal.NewFromString(m.Amount)
|
||||||
|
if err != nil {
|
||||||
|
return merrors.InvalidArgument("invalid decimal amount", "intent.amount")
|
||||||
|
}
|
||||||
|
if amount.IsNegative() {
|
||||||
|
return merrors.InvalidArgument("amount must be >= 0", "intent.amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Validate currency via helper
|
||||||
|
if err := ValidateCurrency(m.Currency, assetResolver); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -36,31 +83,15 @@ type CurrencyPair struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *CurrencyPair) Validate() error {
|
func (p *CurrencyPair) Validate() error {
|
||||||
if p.Base == "" {
|
if p == nil {
|
||||||
return merrors.InvalidArgument("base currency is required", "intent.fx.pair.base")
|
return merrors.InvalidArgument("currency pair is required", "currncy_pair")
|
||||||
}
|
}
|
||||||
if p.Quote == "" {
|
if err := ValidateCurrency(p.Base, &AssetResolverStub{}); err != nil {
|
||||||
return merrors.InvalidArgument("quote currency is required", "intent.fx.pair.quote")
|
return merrors.InvalidArgument("invalid base currency in pair: "+err.Error(), "currency_pair.base")
|
||||||
}
|
}
|
||||||
|
if err := ValidateCurrency(p.Quote, &AssetResolverStub{}); err != nil {
|
||||||
if len(p.Base) != 3 {
|
return merrors.InvalidArgument("invalid quote currency in pair: "+err.Error(), "currency_pair.quote")
|
||||||
return merrors.InvalidArgument("base currency must be 3 letters", "intent.fx.pair.base")
|
|
||||||
}
|
}
|
||||||
if len(p.Quote) != 3 {
|
|
||||||
return merrors.InvalidArgument("quote currency must be 3 letters", "intent.fx.pair.quote")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range p.Base {
|
|
||||||
if c < 'A' || c > 'Z' {
|
|
||||||
return merrors.InvalidArgument("base currency must be uppercase A-Z", "intent.fx.pair.base")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, c := range p.Quote {
|
|
||||||
if c < 'A' || c > 'Z' {
|
|
||||||
return merrors.InvalidArgument("quote currency must be uppercase A-Z", "intent.fx.pair.quote")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,19 @@ func toMoney(m *moneyv1.Money) *model.Money {
|
|||||||
Currency: m.GetCurrency(),
|
Currency: m.GetCurrency(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toMoneyList(list []*moneyv1.Money) []*model.Money {
|
||||||
|
if len(list) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]*model.Money, 0, len(list))
|
||||||
|
for _, item := range list {
|
||||||
|
if m := toMoney(item); m != nil {
|
||||||
|
result = append(result, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,6 +49,19 @@ type PaymentQuote struct {
|
|||||||
FxQuote *FxQuote `json:"fxQuote,omitempty"`
|
FxQuote *FxQuote `json:"fxQuote,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PaymentQuoteAggregate struct {
|
||||||
|
DebitAmounts []*model.Money `json:"debitAmounts,omitempty"`
|
||||||
|
ExpectedSettlementAmounts []*model.Money `json:"expectedSettlementAmounts,omitempty"`
|
||||||
|
ExpectedFeeTotals []*model.Money `json:"expectedFeeTotals,omitempty"`
|
||||||
|
NetworkFeeTotals []*model.Money `json:"networkFeeTotals,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaymentQuotes struct {
|
||||||
|
QuoteRef string `json:"quoteRef,omitempty"`
|
||||||
|
Aggregate *PaymentQuoteAggregate `json:"aggregate,omitempty"`
|
||||||
|
Quotes []PaymentQuote `json:"quotes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type Payment struct {
|
type Payment struct {
|
||||||
PaymentRef string `json:"paymentRef,omitempty"`
|
PaymentRef string `json:"paymentRef,omitempty"`
|
||||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||||
@@ -63,6 +76,16 @@ type paymentQuoteResponse struct {
|
|||||||
Quote *PaymentQuote `json:"quote"`
|
Quote *PaymentQuote `json:"quote"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type paymentQuotesResponse struct {
|
||||||
|
authResponse `json:",inline"`
|
||||||
|
Quote *PaymentQuotes `json:"quote"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type paymentsResponse struct {
|
||||||
|
authResponse `json:",inline"`
|
||||||
|
Payments []Payment `json:"payments"`
|
||||||
|
}
|
||||||
|
|
||||||
type paymentResponse struct {
|
type paymentResponse struct {
|
||||||
authResponse `json:",inline"`
|
authResponse `json:",inline"`
|
||||||
Payment *Payment `json:"payment"`
|
Payment *Payment `json:"payment"`
|
||||||
@@ -76,6 +99,22 @@ func PaymentQuoteResponse(logger mlogger.Logger, quote *orchestratorv1.PaymentQu
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaymentQuotes wraps batch quotes with refreshed access token.
|
||||||
|
func PaymentQuotesResponse(logger mlogger.Logger, resp *orchestratorv1.QuotePaymentsResponse, token *TokenData) http.HandlerFunc {
|
||||||
|
return response.Ok(logger, paymentQuotesResponse{
|
||||||
|
Quote: toPaymentQuotes(resp),
|
||||||
|
authResponse: authResponse{AccessToken: *token},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payments wraps a list of payments with refreshed access token.
|
||||||
|
func PaymentsResponse(logger mlogger.Logger, payments []*orchestratorv1.Payment, token *TokenData) http.HandlerFunc {
|
||||||
|
return response.Ok(logger, paymentsResponse{
|
||||||
|
Payments: toPayments(payments),
|
||||||
|
authResponse: authResponse{AccessToken: *token},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Payment wraps a payment with refreshed access token.
|
// Payment wraps a payment with refreshed access token.
|
||||||
func PaymentResponse(logger mlogger.Logger, payment *orchestratorv1.Payment, token *TokenData) http.HandlerFunc {
|
func PaymentResponse(logger mlogger.Logger, payment *orchestratorv1.Payment, token *TokenData) http.HandlerFunc {
|
||||||
return response.Ok(logger, paymentResponse{
|
return response.Ok(logger, paymentResponse{
|
||||||
@@ -158,6 +197,54 @@ func toPaymentQuote(q *orchestratorv1.PaymentQuote) *PaymentQuote {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toPaymentQuoteAggregate(q *orchestratorv1.PaymentQuoteAggregate) *PaymentQuoteAggregate {
|
||||||
|
if q == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &PaymentQuoteAggregate{
|
||||||
|
DebitAmounts: toMoneyList(q.GetDebitAmounts()),
|
||||||
|
ExpectedSettlementAmounts: toMoneyList(q.GetExpectedSettlementAmounts()),
|
||||||
|
ExpectedFeeTotals: toMoneyList(q.GetExpectedFeeTotals()),
|
||||||
|
NetworkFeeTotals: toMoneyList(q.GetNetworkFeeTotals()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPaymentQuotes(resp *orchestratorv1.QuotePaymentsResponse) *PaymentQuotes {
|
||||||
|
if resp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
quotes := make([]PaymentQuote, 0, len(resp.GetQuotes()))
|
||||||
|
for _, quote := range resp.GetQuotes() {
|
||||||
|
if dto := toPaymentQuote(quote); dto != nil {
|
||||||
|
quotes = append(quotes, *dto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(quotes) == 0 {
|
||||||
|
quotes = nil
|
||||||
|
}
|
||||||
|
return &PaymentQuotes{
|
||||||
|
QuoteRef: resp.GetQuoteRef(),
|
||||||
|
Aggregate: toPaymentQuoteAggregate(resp.GetAggregate()),
|
||||||
|
Quotes: quotes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPayments(items []*orchestratorv1.Payment) []Payment {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]Payment, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
if p := toPayment(item); p != nil {
|
||||||
|
result = append(result, *p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func toPayment(p *orchestratorv1.Payment) *Payment {
|
func toPayment(p *orchestratorv1.Payment) *Payment {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -248,12 +248,11 @@ func (a *AccountAPI) openOrgWallet(ctx context.Context, org *model.Organization,
|
|||||||
a.logger.Warn("Chain gateway client not configured, skipping wallet creation", mzap.StorableRef(org))
|
a.logger.Warn("Chain gateway client not configured, skipping wallet creation", mzap.StorableRef(org))
|
||||||
return merrors.Internal("chain gateway client is not configured")
|
return merrors.Internal("chain gateway client is not configured")
|
||||||
}
|
}
|
||||||
asset := *a.chainAsset
|
|
||||||
req := &chainv1.CreateManagedWalletRequest{
|
req := &chainv1.CreateManagedWalletRequest{
|
||||||
IdempotencyKey: uuid.NewString(),
|
IdempotencyKey: uuid.NewString(),
|
||||||
OrganizationRef: org.ID.Hex(),
|
OrganizationRef: org.ID.Hex(),
|
||||||
OwnerRef: org.ID.Hex(),
|
OwnerRef: org.ID.Hex(),
|
||||||
Asset: &asset,
|
Asset: a.chainAsset,
|
||||||
Metadata: map[string]string{
|
Metadata: map[string]string{
|
||||||
"source": "signup",
|
"source": "signup",
|
||||||
"login": sr.Account.Login,
|
"login": sr.Account.Login,
|
||||||
|
|||||||
74
api/server/internal/server/paymentapiimp/paybatch.go
Normal file
74
api/server/internal/server/paymentapiimp/paybatch.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package paymentapiimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/api/http/response"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
|
"github.com/tech/sendico/server/interface/api/srequest"
|
||||||
|
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||||
|
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
|
||||||
|
orgRef, err := a.oph.GetRef(r)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to parse organization reference for batch payment initiation", zap.Error(err), zap.String(a.oph.Name(), a.oph.GetID(r)))
|
||||||
|
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionCreate)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to check payments access permissions", zap.Error(err), mutil.PLog(a.oph, r))
|
||||||
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
a.logger.Debug("Access denied when initiating batch payments", mutil.PLog(a.oph, r))
|
||||||
|
return response.AccessDenied(a.logger, a.Name(), "payments write permission denied")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := decodeInitiatePaymentsPayload(r)
|
||||||
|
if err != nil {
|
||||||
|
return response.BadPayload(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &orchestratorv1.InitiatePaymentsRequest{
|
||||||
|
Meta: &orchestratorv1.RequestMeta{
|
||||||
|
OrganizationRef: orgRef.Hex(),
|
||||||
|
},
|
||||||
|
IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey),
|
||||||
|
QuoteRef: strings.TrimSpace(payload.QuoteRef),
|
||||||
|
Metadata: payload.Metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := a.client.InitiatePayments(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to initiate batch payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
||||||
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sresponse.PaymentsResponse(a.logger, resp.GetPayments(), token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments, error) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
payload := &srequest.InitiatePayments{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
|
||||||
|
return nil, merrors.InvalidArgument("invalid payload: " + err.Error())
|
||||||
|
}
|
||||||
|
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
|
||||||
|
payload.QuoteRef = strings.TrimSpace(payload.QuoteRef)
|
||||||
|
|
||||||
|
if err := payload.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
@@ -67,6 +67,62 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
|
|||||||
return sresponse.PaymentQuoteResponse(a.logger, resp.GetQuote(), token)
|
return sresponse.PaymentQuoteResponse(a.logger, resp.GetQuote(), token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
|
||||||
|
orgRef, err := a.oph.GetRef(r)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to parse organization reference for quotes", zap.Error(err), zap.String(a.oph.Name(), a.oph.GetID(r)))
|
||||||
|
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionCreate)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to check payments access permissions", zap.Error(err), mutil.PLog(a.oph, r))
|
||||||
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
a.logger.Debug("Access denied when quoting payments", mutil.PLog(a.oph, r))
|
||||||
|
return response.AccessDenied(a.logger, a.Name(), "payments write permission denied")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := decodeQuotePaymentsPayload(r)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Debug("Failed to decode payload", zap.Error(err), mutil.PLog(a.oph, r))
|
||||||
|
return response.BadPayload(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
if err := payload.Validate(); err != nil {
|
||||||
|
a.logger.Debug("Failed to validate payload", zap.Error(err), mutil.PLog(a.oph, r))
|
||||||
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
intents := make([]*orchestratorv1.PaymentIntent, 0, len(payload.Intents))
|
||||||
|
for i := range payload.Intents {
|
||||||
|
intent, err := mapPaymentIntent(&payload.Intents[i])
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Debug("Failed to map payment intent", zap.Error(err), mutil.PLog(a.oph, r))
|
||||||
|
return response.BadPayload(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
intents = append(intents, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &orchestratorv1.QuotePaymentsRequest{
|
||||||
|
Meta: &orchestratorv1.RequestMeta{
|
||||||
|
OrganizationRef: orgRef.Hex(),
|
||||||
|
},
|
||||||
|
IdempotencyKey: payload.IdempotencyKey,
|
||||||
|
Intents: intents,
|
||||||
|
PreviewOnly: payload.PreviewOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := a.client.QuotePayments(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to quote payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
||||||
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sresponse.PaymentQuotesResponse(a.logger, resp, token)
|
||||||
|
}
|
||||||
|
|
||||||
func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
|
func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
|
||||||
@@ -80,3 +136,17 @@ func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
|
|||||||
}
|
}
|
||||||
return payload, nil
|
return payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeQuotePaymentsPayload(r *http.Request) (*srequest.QuotePayments, error) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
payload := &srequest.QuotePayments{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
|
||||||
|
return nil, merrors.InvalidArgument("invalid payload: "+err.Error(), "payload")
|
||||||
|
}
|
||||||
|
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
|
||||||
|
if err := payload.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import (
|
|||||||
|
|
||||||
type paymentClient interface {
|
type paymentClient interface {
|
||||||
QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error)
|
QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error)
|
||||||
|
QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error)
|
||||||
|
InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
|
||||||
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
|
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
@@ -66,8 +68,10 @@ func CreateAPI(apiCtx eapi.API) (*PaymentAPI, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/quote"), api.Post, p.quotePayment)
|
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/quote"), api.Post, p.quotePayment)
|
||||||
|
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/multiquote"), api.Post, p.quotePayments)
|
||||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/immediate"), api.Post, p.initiateImmediate)
|
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/immediate"), api.Post, p.initiateImmediate)
|
||||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-quote"), api.Post, p.initiateByQuote)
|
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-quote"), api.Post, p.initiateByQuote)
|
||||||
|
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-multiquote"), api.Post, p.initiatePaymentsByQuote)
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ services:
|
|||||||
NATS_PASSWORD: ${NATS_PASSWORD}
|
NATS_PASSWORD: ${NATS_PASSWORD}
|
||||||
CHAIN_GATEWAY_ADDRESS: ${CHAIN_GATEWAY_SERVICE_NAME}:${CHAIN_GATEWAY_GRPC_PORT}
|
CHAIN_GATEWAY_ADDRESS: ${CHAIN_GATEWAY_SERVICE_NAME}:${CHAIN_GATEWAY_GRPC_PORT}
|
||||||
LEDGER_ADDRESS: ${LEDGER_SERVICE_NAME}:${LEDGER_GRPC_PORT}
|
LEDGER_ADDRESS: ${LEDGER_SERVICE_NAME}:${LEDGER_GRPC_PORT}
|
||||||
|
PAYMENTS_ADDRESS: ${PAYMENTS_SERVICE_NAME}:${PAYMENTS_GRPC_PORT}
|
||||||
MONGO_HOST: ${MONGO_HOST}
|
MONGO_HOST: ${MONGO_HOST}
|
||||||
MONGO_PORT: ${MONGO_PORT}
|
MONGO_PORT: ${MONGO_PORT}
|
||||||
MONGO_DATABASE: ${MONGO_DATABASE}
|
MONGO_DATABASE: ${MONGO_DATABASE}
|
||||||
|
|||||||
20
frontend/pshared/lib/api/requests/username.dart
Normal file
20
frontend/pshared/lib/api/requests/username.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'username.g.dart';
|
||||||
|
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class ResetUserNameRequest {
|
||||||
|
final String userName;
|
||||||
|
|
||||||
|
const ResetUserNameRequest({
|
||||||
|
required this.userName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ResetUserNameRequest.fromJson(Map<String, dynamic> json) => _$ResetUserNameRequestFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$ResetUserNameRequestToJson(this);
|
||||||
|
|
||||||
|
static ResetUserNameRequest build({
|
||||||
|
required String userName,
|
||||||
|
}) => ResetUserNameRequest(userName: userName);
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ class CommonConstants {
|
|||||||
static String apiEndpoint = '/api/v1';
|
static String apiEndpoint = '/api/v1';
|
||||||
static String amplitudeSecret = 'c3d75b3e2520d708440acbb16b923e79';
|
static String amplitudeSecret = 'c3d75b3e2520d708440acbb16b923e79';
|
||||||
static String amplitudeServerZone = 'EU';
|
static String amplitudeServerZone = 'EU';
|
||||||
|
static String posthogApiKey = 'phc_lVhbruaZpxiQxppHBJpL36ARnPlkqbCewv6cauoceTN';
|
||||||
|
static String posthogHost = 'https://eu.i.posthog.com';
|
||||||
static Locale defaultLocale = const Locale('en');
|
static Locale defaultLocale = const Locale('en');
|
||||||
static String defaultCurrency = 'EUR';
|
static String defaultCurrency = 'EUR';
|
||||||
static int defaultDimensionLength = 500;
|
static int defaultDimensionLength = 500;
|
||||||
@@ -36,6 +38,8 @@ class CommonConstants {
|
|||||||
apiEndpoint = configJson['apiEndpoint'] ?? apiEndpoint;
|
apiEndpoint = configJson['apiEndpoint'] ?? apiEndpoint;
|
||||||
amplitudeSecret = configJson['amplitudeSecret'] ?? amplitudeSecret;
|
amplitudeSecret = configJson['amplitudeSecret'] ?? amplitudeSecret;
|
||||||
amplitudeServerZone = configJson['amplitudeServerZone'] ?? amplitudeServerZone;
|
amplitudeServerZone = configJson['amplitudeServerZone'] ?? amplitudeServerZone;
|
||||||
|
posthogApiKey = configJson['posthogApiKey'] ?? posthogApiKey;
|
||||||
|
posthogHost = configJson['posthogHost'] ?? posthogHost;
|
||||||
defaultLocale = Locale(configJson['defaultLocale'] ?? defaultLocale.languageCode);
|
defaultLocale = Locale(configJson['defaultLocale'] ?? defaultLocale.languageCode);
|
||||||
defaultCurrency = configJson['defaultCurrency'] ?? defaultCurrency;
|
defaultCurrency = configJson['defaultCurrency'] ?? defaultCurrency;
|
||||||
wsProto = configJson['wsProto'] ?? wsProto;
|
wsProto = configJson['wsProto'] ?? wsProto;
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class Constants extends CommonConstants {
|
|||||||
static String get currentOrgKey => CommonConstants.currentOrgKey;
|
static String get currentOrgKey => CommonConstants.currentOrgKey;
|
||||||
static String get apiUrl => CommonConstants.apiUrl;
|
static String get apiUrl => CommonConstants.apiUrl;
|
||||||
static String get serviceUrl => CommonConstants.serviceUrl;
|
static String get serviceUrl => CommonConstants.serviceUrl;
|
||||||
|
static String get posthogApiKey => CommonConstants.posthogApiKey;
|
||||||
|
static String get posthogHost => CommonConstants.posthogHost;
|
||||||
static int get defaultDimensionLength => CommonConstants.defaultDimensionLength;
|
static int get defaultDimensionLength => CommonConstants.defaultDimensionLength;
|
||||||
static String get deviceIdStorageKey => CommonConstants.deviceIdStorageKey;
|
static String get deviceIdStorageKey => CommonConstants.deviceIdStorageKey;
|
||||||
static String get nilObjectRef => CommonConstants.nilObjectRef;
|
static String get nilObjectRef => CommonConstants.nilObjectRef;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ extension AppConfigExtension on AppConfig {
|
|||||||
external String? get apiEndpoint;
|
external String? get apiEndpoint;
|
||||||
external String? get amplitudeSecret;
|
external String? get amplitudeSecret;
|
||||||
external String? get amplitudeServerZone;
|
external String? get amplitudeServerZone;
|
||||||
|
external String? get posthogApiKey;
|
||||||
|
external String? get posthogHost;
|
||||||
external String? get defaultLocale;
|
external String? get defaultLocale;
|
||||||
external String? get wsProto;
|
external String? get wsProto;
|
||||||
external String? get wsEndpoint;
|
external String? get wsEndpoint;
|
||||||
@@ -40,6 +42,8 @@ class Constants extends CommonConstants {
|
|||||||
static String get currentOrgKey => CommonConstants.currentOrgKey;
|
static String get currentOrgKey => CommonConstants.currentOrgKey;
|
||||||
static String get apiUrl => CommonConstants.apiUrl;
|
static String get apiUrl => CommonConstants.apiUrl;
|
||||||
static String get serviceUrl => CommonConstants.serviceUrl;
|
static String get serviceUrl => CommonConstants.serviceUrl;
|
||||||
|
static String get posthogApiKey => CommonConstants.posthogApiKey;
|
||||||
|
static String get posthogHost => CommonConstants.posthogHost;
|
||||||
static int get defaultDimensionLength => CommonConstants.defaultDimensionLength;
|
static int get defaultDimensionLength => CommonConstants.defaultDimensionLength;
|
||||||
static String get deviceIdStorageKey => CommonConstants.deviceIdStorageKey;
|
static String get deviceIdStorageKey => CommonConstants.deviceIdStorageKey;
|
||||||
static String get nilObjectRef => CommonConstants.nilObjectRef;
|
static String get nilObjectRef => CommonConstants.nilObjectRef;
|
||||||
@@ -57,6 +61,8 @@ class Constants extends CommonConstants {
|
|||||||
'apiEndpoint': config.apiEndpoint,
|
'apiEndpoint': config.apiEndpoint,
|
||||||
'amplitudeSecret': config.amplitudeSecret,
|
'amplitudeSecret': config.amplitudeSecret,
|
||||||
'amplitudeServerZone': config.amplitudeServerZone,
|
'amplitudeServerZone': config.amplitudeServerZone,
|
||||||
|
'posthogApiKey': config.posthogApiKey,
|
||||||
|
'posthogHost': config.posthogHost,
|
||||||
'defaultLocale': config.defaultLocale,
|
'defaultLocale': config.defaultLocale,
|
||||||
'wsProto': config.wsProto,
|
'wsProto': config.wsProto,
|
||||||
'wsEndpoint': config.wsEndpoint,
|
'wsEndpoint': config.wsEndpoint,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:pshared/data/dto/wallet/balance.dart';
|
|||||||
import 'package:pshared/data/dto/wallet/wallet.dart';
|
import 'package:pshared/data/dto/wallet/wallet.dart';
|
||||||
import 'package:pshared/data/mapper/wallet/balance.dart';
|
import 'package:pshared/data/mapper/wallet/balance.dart';
|
||||||
import 'package:pshared/data/mapper/wallet/money.dart';
|
import 'package:pshared/data/mapper/wallet/money.dart';
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
import 'package:pshared/models/wallet/wallet.dart';
|
import 'package:pshared/models/wallet/wallet.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -22,5 +23,9 @@ extension WalletDTOMapper on WalletDTO {
|
|||||||
updatedAt: (updatedAt == null || updatedAt!.isEmpty) ? null : DateTime.tryParse(updatedAt!),
|
updatedAt: (updatedAt == null || updatedAt!.isEmpty) ? null : DateTime.tryParse(updatedAt!),
|
||||||
balance: balance?.toDomain(),
|
balance: balance?.toDomain(),
|
||||||
availableMoney: balance?.available?.toDomain(),
|
availableMoney: balance?.available?.toDomain(),
|
||||||
|
describable: newDescribable(
|
||||||
|
name: metadata?['name'] ?? 'Crypto Wallet',
|
||||||
|
description: metadata?['description'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
18
frontend/pshared/lib/models/asset.dart
Normal file
18
frontend/pshared/lib/models/asset.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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),
|
||||||
|
);
|
||||||
7
frontend/pshared/lib/models/auth/state.dart
Normal file
7
frontend/pshared/lib/models/auth/state.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
enum AuthState {
|
||||||
|
idle,
|
||||||
|
checking,
|
||||||
|
ready,
|
||||||
|
empty,
|
||||||
|
error,
|
||||||
|
}
|
||||||
@@ -79,6 +79,10 @@ enum ResourceType {
|
|||||||
@JsonValue('payments')
|
@JsonValue('payments')
|
||||||
payments,
|
payments,
|
||||||
|
|
||||||
|
/// Represents payment orchestration service
|
||||||
|
@JsonValue('payment_orchestrator')
|
||||||
|
paymentOrchestrator,
|
||||||
|
|
||||||
@JsonValue('payment_methods')
|
@JsonValue('payment_methods')
|
||||||
paymentMethods,
|
paymentMethods,
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:pshared/models/wallet/balance.dart';
|
import 'package:pshared/models/wallet/balance.dart';
|
||||||
import 'package:pshared/models/wallet/money.dart';
|
import 'package:pshared/models/wallet/money.dart';
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
|
|
||||||
|
|
||||||
class WalletAsset {
|
class WalletAsset {
|
||||||
@@ -14,7 +15,7 @@ class WalletAsset {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class WalletModel {
|
class WalletModel implements Describable {
|
||||||
final String walletRef;
|
final String walletRef;
|
||||||
final String organizationRef;
|
final String organizationRef;
|
||||||
final String ownerRef;
|
final String ownerRef;
|
||||||
@@ -26,6 +27,13 @@ class WalletModel {
|
|||||||
final DateTime? updatedAt;
|
final DateTime? updatedAt;
|
||||||
final WalletBalance? balance;
|
final WalletBalance? balance;
|
||||||
final WalletMoney? availableMoney;
|
final WalletMoney? availableMoney;
|
||||||
|
final Describable describable;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => describable.name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get description => describable.description;
|
||||||
|
|
||||||
const WalletModel({
|
const WalletModel({
|
||||||
required this.walletRef,
|
required this.walletRef,
|
||||||
@@ -39,11 +47,13 @@ class WalletModel {
|
|||||||
this.updatedAt,
|
this.updatedAt,
|
||||||
this.balance,
|
this.balance,
|
||||||
this.availableMoney,
|
this.availableMoney,
|
||||||
|
required this.describable,
|
||||||
});
|
});
|
||||||
|
|
||||||
WalletModel copyWith({
|
WalletModel copyWith({
|
||||||
WalletBalance? balance,
|
WalletBalance? balance,
|
||||||
WalletMoney? availableMoney,
|
WalletMoney? availableMoney,
|
||||||
|
Describable? describable,
|
||||||
}) {
|
}) {
|
||||||
return WalletModel(
|
return WalletModel(
|
||||||
walletRef: walletRef,
|
walletRef: walletRef,
|
||||||
@@ -57,6 +67,7 @@ class WalletModel {
|
|||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
balance: balance ?? this.balance,
|
balance: balance ?? this.balance,
|
||||||
availableMoney: availableMoney ?? this.availableMoney,
|
availableMoney: availableMoney ?? this.availableMoney,
|
||||||
|
describable: describable ?? this.describable,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pshared/models/auth/state.dart';
|
||||||
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
@@ -20,8 +23,14 @@ import 'package:pshared/utils/exception.dart';
|
|||||||
|
|
||||||
|
|
||||||
class AccountProvider extends ChangeNotifier {
|
class AccountProvider extends ChangeNotifier {
|
||||||
|
AccountProvider();
|
||||||
|
|
||||||
static String get currentUserRef => Constants.nilObjectRef;
|
static String get currentUserRef => Constants.nilObjectRef;
|
||||||
|
|
||||||
|
/// Auth lifecycle state to avoid multiple ad-hoc flags.
|
||||||
|
AuthState _authState = AuthState.idle;
|
||||||
|
AuthState get authState => _authState;
|
||||||
|
|
||||||
// The resource now wraps our Account? state along with its loading/error state.
|
// The resource now wraps our Account? state along with its loading/error state.
|
||||||
Resource<Account?> _resource = Resource(data: null);
|
Resource<Account?> _resource = Resource(data: null);
|
||||||
Resource<Account?> get resource => _resource;
|
Resource<Account?> get resource => _resource;
|
||||||
@@ -52,9 +61,18 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private helper to update the resource and notify listeners.
|
@protected
|
||||||
|
Future<void> onAccountChanged(Account? previous, Account? current) => Future<void>.value();
|
||||||
|
|
||||||
void _setResource(Resource<Account?> newResource) {
|
void _setResource(Resource<Account?> newResource) {
|
||||||
|
final previousAccount = _resource.data;
|
||||||
_resource = newResource;
|
_resource = newResource;
|
||||||
|
final currentAccount = newResource.data;
|
||||||
|
|
||||||
|
if (previousAccount != currentAccount) {
|
||||||
|
unawaited(onAccountChanged(previousAccount, currentAccount));
|
||||||
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +93,7 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
locale: locale,
|
locale: locale,
|
||||||
));
|
));
|
||||||
if (outcome.account != null) {
|
if (outcome.account != null) {
|
||||||
|
_authState = AuthState.ready;
|
||||||
_setResource(Resource(data: outcome.account, isLoading: false));
|
_setResource(Resource(data: outcome.account, isLoading: false));
|
||||||
_pickupLocale(outcome.account!.locale);
|
_pickupLocale(outcome.account!.locale);
|
||||||
} else {
|
} else {
|
||||||
@@ -84,10 +103,12 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
await VerificationService.requestLoginCode(pending);
|
await VerificationService.requestLoginCode(pending);
|
||||||
_pendingLogin = pending;
|
_pendingLogin = pending;
|
||||||
|
_authState = AuthState.idle;
|
||||||
_setResource(_resource.copyWith(isLoading: false));
|
_setResource(_resource.copyWith(isLoading: false));
|
||||||
}
|
}
|
||||||
return outcome;
|
return outcome;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
_authState = AuthState.error;
|
||||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -95,6 +116,7 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
void completePendingLogin(Account account) {
|
void completePendingLogin(Account account) {
|
||||||
_pendingLogin = null;
|
_pendingLogin = null;
|
||||||
|
_authState = AuthState.ready;
|
||||||
_setResource(Resource(data: account, isLoading: false, error: null));
|
_setResource(Resource(data: account, isLoading: false, error: null));
|
||||||
_pickupLocale(account.locale);
|
_pickupLocale(account.locale);
|
||||||
}
|
}
|
||||||
@@ -102,13 +124,17 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
Future<bool> isAuthorizationStored() async => AuthorizationService.isAuthorizationStored();
|
Future<bool> isAuthorizationStored() async => AuthorizationService.isAuthorizationStored();
|
||||||
|
|
||||||
Future<Account?> restore() async {
|
Future<Account?> restore() async {
|
||||||
|
_authState = AuthState.checking;
|
||||||
|
notifyListeners();
|
||||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||||
try {
|
try {
|
||||||
final acc = await AccountService.restore();
|
final acc = await AccountService.restore();
|
||||||
|
_authState = AuthState.ready;
|
||||||
_setResource(Resource(data: acc, isLoading: false));
|
_setResource(Resource(data: acc, isLoading: false));
|
||||||
_pickupLocale(acc.locale);
|
_pickupLocale(acc.locale);
|
||||||
return acc;
|
return acc;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
_authState = AuthState.error;
|
||||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -140,11 +166,14 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
|
_authState = AuthState.empty;
|
||||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||||
|
_pendingLogin = null;
|
||||||
try {
|
try {
|
||||||
await AccountService.logout();
|
await AccountService.logout();
|
||||||
_setResource(Resource(data: null, isLoading: false));
|
_setResource(Resource(data: null, isLoading: false));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
_authState = AuthState.error;
|
||||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -199,6 +228,19 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Account> resetUsername(String userName) async {
|
||||||
|
if (account == null) throw ErrorUnauthorized();
|
||||||
|
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||||
|
try {
|
||||||
|
final updated = await AccountService.resetUsername(account!, userName);
|
||||||
|
_setResource(Resource(data: updated, isLoading: false));
|
||||||
|
return updated;
|
||||||
|
} catch (e) {
|
||||||
|
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> forgotPassword(String email) async {
|
Future<void> forgotPassword(String email) async {
|
||||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||||
try {
|
try {
|
||||||
@@ -220,4 +262,15 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> restoreIfPossible() async {
|
||||||
|
if (_authState == AuthState.checking || _authState == AuthState.ready) return;
|
||||||
|
final hasAuth = await AuthorizationService.isAuthorizationStored();
|
||||||
|
if (!hasAuth) {
|
||||||
|
_authState = AuthState.empty;
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await restore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,4 +80,12 @@ class OrganizationsProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> reset() async {
|
||||||
|
_resource = Resource(data: []);
|
||||||
|
_currentOrg = null;
|
||||||
|
notifyListeners();
|
||||||
|
// Best-effort cleanup of stored selection to avoid using stale org on next login.
|
||||||
|
await SecureStorageService.delete(Constants.currentOrgKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
frontend/pshared/lib/provider/payment/amount.dart
Normal file
20
frontend/pshared/lib/provider/payment/amount.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentAmountProvider with ChangeNotifier {
|
||||||
|
double _amount = 10.0;
|
||||||
|
bool _payerCoversFee = true;
|
||||||
|
|
||||||
|
double get amount => _amount;
|
||||||
|
bool get payerCoversFee => _payerCoversFee;
|
||||||
|
|
||||||
|
void setAmount(double value) {
|
||||||
|
_amount = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPayerCoversFee(bool value) {
|
||||||
|
_payerCoversFee = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pshared/models/asset.dart';
|
||||||
|
|
||||||
|
import 'package:uuid/uuid.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/payment/kind.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/card.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||||
|
import 'package:pshared/models/payment/money.dart';
|
||||||
|
import 'package:pshared/models/payment/settlement_mode.dart';
|
||||||
|
import 'package:pshared/provider/payment/amount.dart';
|
||||||
import 'package:pshared/api/requests/payment/quote.dart';
|
import 'package:pshared/api/requests/payment/quote.dart';
|
||||||
import 'package:pshared/data/mapper/payment/intent/payment.dart';
|
import 'package:pshared/data/mapper/payment/intent/payment.dart';
|
||||||
import 'package:pshared/models/payment/intent.dart';
|
import 'package:pshared/models/payment/intent.dart';
|
||||||
@@ -7,7 +19,6 @@ import 'package:pshared/models/payment/quote.dart';
|
|||||||
import 'package:pshared/provider/organizations.dart';
|
import 'package:pshared/provider/organizations.dart';
|
||||||
import 'package:pshared/provider/resource.dart';
|
import 'package:pshared/provider/resource.dart';
|
||||||
import 'package:pshared/service/payment/quotation.dart';
|
import 'package:pshared/service/payment/quotation.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class QuotationProvider extends ChangeNotifier {
|
class QuotationProvider extends ChangeNotifier {
|
||||||
@@ -15,14 +26,46 @@ class QuotationProvider extends ChangeNotifier {
|
|||||||
late OrganizationsProvider _organizations;
|
late OrganizationsProvider _organizations;
|
||||||
bool _isLoaded = false;
|
bool _isLoaded = false;
|
||||||
|
|
||||||
void update(OrganizationsProvider venue) {
|
void update(OrganizationsProvider venue, PaymentAmountProvider payment) {
|
||||||
_organizations = venue;
|
_organizations = venue;
|
||||||
|
getQuotation(PaymentIntent(
|
||||||
|
kind: PaymentKind.payout,
|
||||||
|
amount: Money(
|
||||||
|
amount: payment.amount.toString(),
|
||||||
|
currency: 'USDT',
|
||||||
|
),
|
||||||
|
destination: CardPaymentMethod(
|
||||||
|
pan: '4000000000000077',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
),
|
||||||
|
source: ManagedWalletPaymentMethod(
|
||||||
|
managedWalletRef: '',
|
||||||
|
),
|
||||||
|
fx: FxIntent(
|
||||||
|
pair: CurrencyPair(
|
||||||
|
base: 'USDT',
|
||||||
|
quote: 'RUB',
|
||||||
|
),
|
||||||
|
side: FxSide.sellBaseBuyQuote,
|
||||||
|
),
|
||||||
|
settlementMode: payment.payerCoversFee ? SettlementMode.fixReceived : SettlementMode.fixSource,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
PaymentQuote? get quotation => _quotation.data;
|
PaymentQuote? get quotation => _quotation.data;
|
||||||
|
|
||||||
bool get isReady => _isLoaded && !_quotation.isLoading && _quotation.error == null;
|
bool get isReady => _isLoaded && !_quotation.isLoading && _quotation.error == null;
|
||||||
|
|
||||||
|
Asset? get fee => quotation == null ? null : createAsset(quotation!.expectedFeeTotal!.currency, quotation!.expectedFeeTotal!.amount);
|
||||||
|
Asset? get total => quotation == null ? null : createAsset(quotation!.debitAmount!.currency, quotation!.debitAmount!.amount);
|
||||||
|
Asset? get recipientGets => quotation == null ? null : createAsset(quotation!.expectedSettlementAmount!.currency, quotation!.expectedSettlementAmount!.amount);
|
||||||
|
|
||||||
|
void _setResource(Resource<PaymentQuote> quotation) {
|
||||||
|
_quotation = quotation;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<PaymentQuote?> getQuotation(PaymentIntent intent) async {
|
Future<PaymentQuote?> getQuotation(PaymentIntent intent) async {
|
||||||
if (!_organizations.isOrganizationSet) throw StateError('Organization is not set');
|
if (!_organizations.isOrganizationSet) throw StateError('Organization is not set');
|
||||||
try {
|
try {
|
||||||
@@ -35,19 +78,20 @@ class QuotationProvider extends ChangeNotifier {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
_isLoaded = true;
|
_isLoaded = true;
|
||||||
_quotation = _quotation.copyWith(data: response, isLoading: false);
|
_setResource(_quotation.copyWith(data: response, isLoading: false, error: null));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_quotation = _quotation.copyWith(
|
_setResource(_quotation.copyWith(
|
||||||
|
data: null,
|
||||||
error: e is Exception ? e : Exception(e.toString()),
|
error: e is Exception ? e : Exception(e.toString()),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return _quotation.data;
|
return _quotation.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
_quotation = Resource(data: null, isLoading: false, error: null);
|
_setResource(Resource(data: null, isLoading: false, error: null));
|
||||||
_isLoaded = false;
|
_isLoaded = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@@ -21,9 +23,17 @@ class PermissionsProvider extends ChangeNotifier {
|
|||||||
Resource<UserAccess> _userAccess = Resource(data: null, isLoading: false, error: null);
|
Resource<UserAccess> _userAccess = Resource(data: null, isLoading: false, error: null);
|
||||||
late OrganizationsProvider _organizations;
|
late OrganizationsProvider _organizations;
|
||||||
bool _isLoaded = false;
|
bool _isLoaded = false;
|
||||||
|
String? _loadedOrgRef;
|
||||||
|
//For permissions to auto-load when an organization is set, so the dashboard no longer hangs waiting for permissions to become ready.
|
||||||
|
|
||||||
void update(OrganizationsProvider venue) {
|
void update(OrganizationsProvider venue) {
|
||||||
_organizations = venue;
|
_organizations = venue;
|
||||||
|
// Trigger a reload when organization changes or when permissions were never loaded.
|
||||||
|
if (_organizations.isOrganizationSet &&
|
||||||
|
_loadedOrgRef != _organizations.current.id &&
|
||||||
|
!_userAccess.isLoading) {
|
||||||
|
unawaited(load());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic wrapper to perform service calls and reload state
|
// Generic wrapper to perform service calls and reload state
|
||||||
@@ -43,6 +53,10 @@ class PermissionsProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
/// Load the [UserAccess] for the current venue.
|
/// Load the [UserAccess] for the current venue.
|
||||||
Future<UserAccess?> load() async {
|
Future<UserAccess?> load() async {
|
||||||
|
if (!_organizations.isOrganizationSet) {
|
||||||
|
// Organization is not ready yet; skip loading until it becomes available.
|
||||||
|
return _userAccess.data;
|
||||||
|
}
|
||||||
_userAccess = _userAccess.copyWith(isLoading: true, error: null);
|
_userAccess = _userAccess.copyWith(isLoading: true, error: null);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
@@ -56,6 +70,7 @@ class PermissionsProvider extends ChangeNotifier {
|
|||||||
_userAccess = _userAccess.copyWith(data: allAccess, isLoading: false);
|
_userAccess = _userAccess.copyWith(data: allAccess, isLoading: false);
|
||||||
}
|
}
|
||||||
_isLoaded = true;
|
_isLoaded = true;
|
||||||
|
_loadedOrgRef = orgRef;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_userAccess = _userAccess.copyWith(
|
_userAccess = _userAccess.copyWith(
|
||||||
error: e is Exception ? e : Exception(e.toString()),
|
error: e is Exception ? e : Exception(e.toString()),
|
||||||
@@ -164,9 +179,14 @@ class PermissionsProvider extends ChangeNotifier {
|
|||||||
void reset() {
|
void reset() {
|
||||||
_userAccess = Resource(data: null, isLoading: false, error: null);
|
_userAccess = Resource(data: null, isLoading: false, error: null);
|
||||||
_isLoaded = false;
|
_isLoaded = false;
|
||||||
|
_loadedOrgRef = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> resetAsync() async {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
bool canRead(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.read, objectRef: objectRef);
|
bool canRead(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.read, objectRef: objectRef);
|
||||||
bool canUpdate(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.update, objectRef: objectRef);
|
bool canUpdate(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.update, objectRef: objectRef);
|
||||||
bool canDelete(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.delete, objectRef: objectRef);
|
bool canDelete(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.delete, objectRef: objectRef);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
import 'package:pshared/api/requests/signup.dart';
|
import 'package:pshared/api/requests/signup.dart';
|
||||||
@@ -10,6 +9,7 @@ import 'package:pshared/api/requests/password/forgot.dart';
|
|||||||
import 'package:pshared/api/requests/password/reset.dart';
|
import 'package:pshared/api/requests/password/reset.dart';
|
||||||
import 'package:pshared/data/mapper/account/account.dart';
|
import 'package:pshared/data/mapper/account/account.dart';
|
||||||
import 'package:pshared/models/account/account.dart';
|
import 'package:pshared/models/account/account.dart';
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
import 'package:pshared/models/auth/login_outcome.dart';
|
import 'package:pshared/models/auth/login_outcome.dart';
|
||||||
import 'package:pshared/service/authorization/service.dart';
|
import 'package:pshared/service/authorization/service.dart';
|
||||||
import 'package:pshared/service/files.dart';
|
import 'package:pshared/service/files.dart';
|
||||||
@@ -61,6 +61,14 @@ class AccountService {
|
|||||||
await getPOSTResponse(_objectType, 'password/reset/$accountRef/$token', ResetPasswordRequest.build(password: newPassword).toJson());
|
await getPOSTResponse(_objectType, 'password/reset/$accountRef/$token', ResetPasswordRequest.build(password: newPassword).toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<Account> resetUsername(Account account, String userName) async {
|
||||||
|
_logger.fine('Updating username for account: ${account.id}');
|
||||||
|
final updatedAccount = account.copyWith(
|
||||||
|
describable: account.describable.copyWith(name: userName),
|
||||||
|
);
|
||||||
|
return update(updatedAccount);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Account> changePassword(String oldPassword, String newPassword) async {
|
static Future<Account> changePassword(String oldPassword, String newPassword) async {
|
||||||
_logger.fine('Changing password');
|
_logger.fine('Changing password');
|
||||||
return _getAccount(AuthorizationService.getPATCHResponse(
|
return _getAccount(AuthorizationService.getPATCHResponse(
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import 'package:pshared/api/requests/payment/quote.dart';
|
|||||||
import 'package:pshared/api/responses/payment/quotation.dart';
|
import 'package:pshared/api/responses/payment/quotation.dart';
|
||||||
import 'package:pshared/data/mapper/payment/payment_quote.dart';
|
import 'package:pshared/data/mapper/payment/payment_quote.dart';
|
||||||
import 'package:pshared/models/payment/quote.dart';
|
import 'package:pshared/models/payment/quote.dart';
|
||||||
|
import 'package:pshared/service/authorization/service.dart';
|
||||||
import 'package:pshared/service/services.dart';
|
import 'package:pshared/service/services.dart';
|
||||||
import 'package:pshared/utils/http/requests.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class QuotationService {
|
class QuotationService {
|
||||||
@@ -14,7 +14,11 @@ class QuotationService {
|
|||||||
|
|
||||||
static Future<PaymentQuote> getQuotation(String organizationRef, QuotePaymentRequest request) async {
|
static Future<PaymentQuote> getQuotation(String organizationRef, QuotePaymentRequest request) async {
|
||||||
_logger.fine('Quoting payment for organization $organizationRef');
|
_logger.fine('Quoting payment for organization $organizationRef');
|
||||||
final response = await getPOSTResponse(_objectType, '/quote/$organizationRef', request.toJson());
|
final response = await AuthorizationService.getPOSTResponse(
|
||||||
|
_objectType,
|
||||||
|
'/quote/$organizationRef',
|
||||||
|
request.toJson(),
|
||||||
|
);
|
||||||
return PaymentQuoteResponse.fromJson(response).quote.toDomain();
|
return PaymentQuoteResponse.fromJson(response).quote.toDomain();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,64 @@
|
|||||||
String currencyCodeToSymbol(String currencyCode) {
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/asset.dart';
|
||||||
|
import 'package:pshared/models/currency.dart';
|
||||||
|
|
||||||
|
|
||||||
|
String currencyCodeToSymbol(Currency currencyCode) {
|
||||||
switch (currencyCode) {
|
switch (currencyCode) {
|
||||||
case 'USD':
|
case Currency.usd:
|
||||||
return '\$';
|
return '\$';
|
||||||
case 'PLN':
|
case Currency.usdt:
|
||||||
return 'zł';
|
return '₮';
|
||||||
case 'EUR':
|
case Currency.usdc:
|
||||||
return '€';
|
return '\$';
|
||||||
case 'GBP':
|
case Currency.rub:
|
||||||
return '£';
|
|
||||||
case 'HUF':
|
|
||||||
return 'Ft';
|
|
||||||
case 'RUB':
|
|
||||||
return '₽';
|
return '₽';
|
||||||
default:
|
case Currency.eur:
|
||||||
return currencyCode;
|
return '€';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String currencyToString(String currencyCode, double amount) {
|
String amountToString(double amount) {
|
||||||
return '${currencyCodeToSymbol(currencyCode)}\u00A0${amount.toStringAsFixed(2)}';
|
return amount.toStringAsFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
String currencyToString(Currency currencyCode, double amount) {
|
||||||
|
return '${currencyCodeToSymbol(currencyCode)}\u00A0${amountToString(amount)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ RouteBase payoutShellRoute() => ShellRoute(
|
|||||||
pageBuilder: (context, _) {
|
pageBuilder: (context, _) {
|
||||||
final recipient = context.read<RecipientsProvider>().currentObject;
|
final recipient = context.read<RecipientsProvider>().currentObject;
|
||||||
return NoTransitionPage(
|
return NoTransitionPage(
|
||||||
child: AdressBookRecipientForm(
|
child: AddressBookRecipientForm(
|
||||||
recipient: recipient,
|
recipient: recipient,
|
||||||
onSaved: (_) => context.goToPayout(PayoutDestination.recipients),
|
onSaved: (_) => context.goToPayout(PayoutDestination.recipients),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:pshared/models/wallet/wallet.dart' as domain;
|
import 'package:pshared/models/wallet/wallet.dart' as domain;
|
||||||
|
|
||||||
import 'package:pweb/models/currency.dart';
|
import 'package:pshared/models/currency.dart';
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
|
|
||||||
import 'package:pweb/models/wallet.dart';
|
import 'package:pweb/models/wallet.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -16,7 +18,6 @@ extension WalletUiMapper on domain.WalletModel {
|
|||||||
return Wallet(
|
return Wallet(
|
||||||
id: walletRef,
|
id: walletRef,
|
||||||
walletUserID: walletRef,
|
walletUserID: walletRef,
|
||||||
name: metadata?['name'] ?? walletRef,
|
|
||||||
balance: parsedAmount,
|
balance: parsedAmount,
|
||||||
currency: currency,
|
currency: currency,
|
||||||
isHidden: true,
|
isHidden: true,
|
||||||
@@ -25,6 +26,10 @@ extension WalletUiMapper on domain.WalletModel {
|
|||||||
network: asset.chain,
|
network: asset.chain,
|
||||||
tokenSymbol: asset.tokenSymbol,
|
tokenSymbol: asset.tokenSymbol,
|
||||||
contractAddress: asset.contractAddress,
|
contractAddress: asset.contractAddress,
|
||||||
|
describable: newDescribable(
|
||||||
|
name: metadata?['name'] ?? 'Crypto Wallet',
|
||||||
|
description: metadata?['description'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,13 @@
|
|||||||
"usernameErrorInvalid": "Provide a valid email address",
|
"usernameErrorInvalid": "Provide a valid email address",
|
||||||
"usernameUnknownTLD": "Domain .{domain} is not known, please, check it",
|
"usernameUnknownTLD": "Domain .{domain} is not known, please, check it",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
|
"oldPassword": "Current password",
|
||||||
|
"newPassword": "New password",
|
||||||
"confirmPassword": "Confirm password",
|
"confirmPassword": "Confirm password",
|
||||||
|
"changePassword": "Change password",
|
||||||
|
"savePassword": "Save changed password",
|
||||||
|
"changePasswordSuccess": "Password updated",
|
||||||
|
"changePasswordError": "Could not update password",
|
||||||
"passwordValidationRuleDigit": "has digit",
|
"passwordValidationRuleDigit": "has digit",
|
||||||
"passwordValidationRuleUpperCase": "has uppercase letter",
|
"passwordValidationRuleUpperCase": "has uppercase letter",
|
||||||
"passwordValidationRuleLowerCase": "has lowercase letter",
|
"passwordValidationRuleLowerCase": "has lowercase letter",
|
||||||
@@ -311,6 +317,7 @@
|
|||||||
"paymentTypeBankAccount": "Russian Bank Account",
|
"paymentTypeBankAccount": "Russian Bank Account",
|
||||||
"paymentTypeIban": "IBAN",
|
"paymentTypeIban": "IBAN",
|
||||||
"paymentTypeWallet": "Wallet",
|
"paymentTypeWallet": "Wallet",
|
||||||
|
"paymentTypeCryptoWallet": "Crypto Wallet",
|
||||||
"paymentTypeCryptoAddress": "Crypto address",
|
"paymentTypeCryptoAddress": "Crypto address",
|
||||||
"paymentTypeLedger": "Ledger account",
|
"paymentTypeLedger": "Ledger account",
|
||||||
"paymentTypeManagedWallet": "Managed wallet",
|
"paymentTypeManagedWallet": "Managed wallet",
|
||||||
@@ -378,7 +385,7 @@
|
|||||||
"send": "Send Payout",
|
"send": "Send Payout",
|
||||||
"recipientPaysFee": "Recipient pays the fee",
|
"recipientPaysFee": "Recipient pays the fee",
|
||||||
|
|
||||||
"sentAmount": "Sent amount: ${amount}",
|
"sentAmount": "Sent amount: {amount}",
|
||||||
"@sentAmount": {
|
"@sentAmount": {
|
||||||
"description": "Label showing the amount sent",
|
"description": "Label showing the amount sent",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -388,7 +395,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"fee": "Fee: ${fee}",
|
"fee": "Fee: {fee}",
|
||||||
"@fee": {
|
"@fee": {
|
||||||
"description": "Label showing the transaction fee",
|
"description": "Label showing the transaction fee",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -398,7 +405,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"recipientWillReceive": "Recipient will receive: ${amount}",
|
"recipientWillReceive": "Recipient will receive: {amount}",
|
||||||
"@recipientWillReceive": {
|
"@recipientWillReceive": {
|
||||||
"description": "Label showing how much the recipient will receive",
|
"description": "Label showing how much the recipient will receive",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -408,7 +415,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"total": "Total: ${total}",
|
"total": "Total: {total}",
|
||||||
"@total": {
|
"@total": {
|
||||||
"description": "Label showing the total amount of the transaction",
|
"description": "Label showing the total amount of the transaction",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -428,6 +435,10 @@
|
|||||||
|
|
||||||
"choosePaymentMethod": "Payment Methods (choose at least 1)",
|
"choosePaymentMethod": "Payment Methods (choose at least 1)",
|
||||||
"recipientFormRule": "Recipient must have at least one payment method",
|
"recipientFormRule": "Recipient must have at least one payment method",
|
||||||
|
"recipientFormValidationError": "Fill in the required fields to save the recipient",
|
||||||
|
"@recipientFormValidationError": {
|
||||||
|
"description": "Error message shown when recipient form fields are invalid or missing"
|
||||||
|
},
|
||||||
|
|
||||||
"allStatus": "All",
|
"allStatus": "All",
|
||||||
"readyStatus": "Ready",
|
"readyStatus": "Ready",
|
||||||
@@ -443,6 +454,10 @@
|
|||||||
"@errorSaveRecipient": {
|
"@errorSaveRecipient": {
|
||||||
"description": "Error message displayed when saving a recipient fails"
|
"description": "Error message displayed when saving a recipient fails"
|
||||||
},
|
},
|
||||||
|
"recipientSavedSuccessfully": "Recipient saved successfully",
|
||||||
|
"@recipientSavedSuccessfully": {
|
||||||
|
"description": "Success message displayed when a recipient is saved"
|
||||||
|
},
|
||||||
"recipientDeletedSuccessfully": "Recipient deleted successfully",
|
"recipientDeletedSuccessfully": "Recipient deleted successfully",
|
||||||
"@recipientDeletedSuccessfully": {
|
"@recipientDeletedSuccessfully": {
|
||||||
"description": "Success message displayed when a recipient is deleted"
|
"description": "Success message displayed when a recipient is deleted"
|
||||||
|
|||||||
@@ -9,7 +9,13 @@
|
|||||||
"usernameErrorInvalid": "Укажите действительный адрес электронной почты",
|
"usernameErrorInvalid": "Укажите действительный адрес электронной почты",
|
||||||
"usernameUnknownTLD": "Домен .{domain} неизвестен, пожалуйста, проверьте его",
|
"usernameUnknownTLD": "Домен .{domain} неизвестен, пожалуйста, проверьте его",
|
||||||
"password": "Пароль",
|
"password": "Пароль",
|
||||||
|
"oldPassword": "Текущий пароль",
|
||||||
|
"newPassword": "Новый пароль",
|
||||||
"confirmPassword": "Подтвердите пароль",
|
"confirmPassword": "Подтвердите пароль",
|
||||||
|
"changePassword": "Изменить пароль",
|
||||||
|
"savePassword": "Сохранить пароль",
|
||||||
|
"changePasswordSuccess": "Пароль обновлен",
|
||||||
|
"changePasswordError": "Не удалось обновить пароль",
|
||||||
"passwordValidationRuleDigit": "содержит цифру",
|
"passwordValidationRuleDigit": "содержит цифру",
|
||||||
"passwordValidationRuleUpperCase": "содержит заглавную букву",
|
"passwordValidationRuleUpperCase": "содержит заглавную букву",
|
||||||
"passwordValidationRuleLowerCase": "содержит строчную букву",
|
"passwordValidationRuleLowerCase": "содержит строчную букву",
|
||||||
@@ -311,6 +317,7 @@
|
|||||||
"paymentTypeBankAccount": "Российский банковский счет",
|
"paymentTypeBankAccount": "Российский банковский счет",
|
||||||
"paymentTypeIban": "IBAN",
|
"paymentTypeIban": "IBAN",
|
||||||
"paymentTypeWallet": "Кошелек",
|
"paymentTypeWallet": "Кошелек",
|
||||||
|
"paymentTypeCryptoWallet": "Криптокошелек",
|
||||||
"paymentTypeCryptoAddress": "Крипто-адрес",
|
"paymentTypeCryptoAddress": "Крипто-адрес",
|
||||||
"paymentTypeLedger": "Леджер счет",
|
"paymentTypeLedger": "Леджер счет",
|
||||||
"paymentTypeManagedWallet": "Управляемый кошелек",
|
"paymentTypeManagedWallet": "Управляемый кошелек",
|
||||||
@@ -378,7 +385,7 @@
|
|||||||
"send": "Отправить выплату",
|
"send": "Отправить выплату",
|
||||||
"recipientPaysFee": "Получатель оплачивает комиссию",
|
"recipientPaysFee": "Получатель оплачивает комиссию",
|
||||||
|
|
||||||
"sentAmount": "Отправленная сумма: ${amount}",
|
"sentAmount": "Отправленная сумма: {amount}",
|
||||||
"@sentAmount": {
|
"@sentAmount": {
|
||||||
"description": "Метка, показывающая отправленную сумму",
|
"description": "Метка, показывающая отправленную сумму",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -388,7 +395,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"fee": "Комиссия: ${fee}",
|
"fee": "Комиссия: {fee}",
|
||||||
"@fee": {
|
"@fee": {
|
||||||
"description": "Метка, показывающая комиссию за транзакцию",
|
"description": "Метка, показывающая комиссию за транзакцию",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -398,7 +405,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"recipientWillReceive": "Получатель получит: ${amount}",
|
"recipientWillReceive": "Получатель получит: {amount}",
|
||||||
"@recipientWillReceive": {
|
"@recipientWillReceive": {
|
||||||
"description": "Метка, показывающая, сколько получит получатель",
|
"description": "Метка, показывающая, сколько получит получатель",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -408,7 +415,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"total": "Итого: ${total}",
|
"total": "Итого: {total}",
|
||||||
"@total": {
|
"@total": {
|
||||||
"description": "Метка, показывающая общую сумму транзакции",
|
"description": "Метка, показывающая общую сумму транзакции",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -428,6 +435,10 @@
|
|||||||
|
|
||||||
"choosePaymentMethod": "Способы оплаты (выберите хотя бы 1)",
|
"choosePaymentMethod": "Способы оплаты (выберите хотя бы 1)",
|
||||||
"recipientFormRule": "Получатель должен иметь хотя бы один способ оплаты",
|
"recipientFormRule": "Получатель должен иметь хотя бы один способ оплаты",
|
||||||
|
"recipientFormValidationError": "Заполните обязательные поля, чтобы сохранить получателя",
|
||||||
|
"@recipientFormValidationError": {
|
||||||
|
"description": "Сообщение об ошибке, если обязательные поля формы получателя не заполнены"
|
||||||
|
},
|
||||||
|
|
||||||
"allStatus": "Все",
|
"allStatus": "Все",
|
||||||
"readyStatus": "Готов",
|
"readyStatus": "Готов",
|
||||||
@@ -443,6 +454,10 @@
|
|||||||
"@errorSaveRecipient": {
|
"@errorSaveRecipient": {
|
||||||
"description": "Сообщение об ошибке при неудачном сохранении получателя"
|
"description": "Сообщение об ошибке при неудачном сохранении получателя"
|
||||||
},
|
},
|
||||||
|
"recipientSavedSuccessfully": "Получатель успешно сохранен",
|
||||||
|
"@recipientSavedSuccessfully": {
|
||||||
|
"description": "Сообщение об успешном сохранении получателя"
|
||||||
|
},
|
||||||
"recipientDeletedSuccessfully": "Получатель успешно удален",
|
"recipientDeletedSuccessfully": "Получатель успешно удален",
|
||||||
"@recipientDeletedSuccessfully": {
|
"@recipientDeletedSuccessfully": {
|
||||||
"description": "Сообщение об успешном удалении получателя"
|
"description": "Сообщение об успешном удалении получателя"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:pshared/provider/locale.dart';
|
|||||||
import 'package:pshared/provider/permissions.dart';
|
import 'package:pshared/provider/permissions.dart';
|
||||||
import 'package:pshared/provider/account.dart';
|
import 'package:pshared/provider/account.dart';
|
||||||
import 'package:pshared/provider/organizations.dart';
|
import 'package:pshared/provider/organizations.dart';
|
||||||
|
import 'package:pshared/provider/payment/amount.dart';
|
||||||
import 'package:pshared/provider/payment/quotation.dart';
|
import 'package:pshared/provider/payment/quotation.dart';
|
||||||
import 'package:pshared/provider/recipient/provider.dart';
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
import 'package:pshared/provider/recipient/pmethods.dart';
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
@@ -27,8 +28,10 @@ import 'package:pweb/providers/wallets.dart';
|
|||||||
import 'package:pweb/providers/wallet_transactions.dart';
|
import 'package:pweb/providers/wallet_transactions.dart';
|
||||||
import 'package:pweb/services/operations.dart';
|
import 'package:pweb/services/operations.dart';
|
||||||
import 'package:pweb/services/payments/history.dart';
|
import 'package:pweb/services/payments/history.dart';
|
||||||
|
import 'package:pweb/services/posthog.dart';
|
||||||
import 'package:pweb/services/wallet_transactions.dart';
|
import 'package:pweb/services/wallet_transactions.dart';
|
||||||
import 'package:pweb/services/wallets.dart';
|
import 'package:pweb/services/wallets.dart';
|
||||||
|
import 'package:pweb/providers/account.dart';
|
||||||
|
|
||||||
|
|
||||||
void _setupLogging() {
|
void _setupLogging() {
|
||||||
@@ -40,11 +43,9 @@ void _setupLogging() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
await Constants.initialize();
|
|
||||||
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await Constants.initialize();
|
||||||
// await AmplitudeService.initialize();
|
await PosthogService.initialize();
|
||||||
|
|
||||||
|
|
||||||
_setupLogging();
|
_setupLogging();
|
||||||
@@ -57,7 +58,7 @@ void main() async {
|
|||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (_) => LocaleProvider(null)),
|
ChangeNotifierProvider(create: (_) => LocaleProvider(null)),
|
||||||
ChangeNotifierProxyProvider<LocaleProvider, AccountProvider>(
|
ChangeNotifierProxyProvider<LocaleProvider, AccountProvider>(
|
||||||
create: (_) => AccountProvider(),
|
create: (_) => PwebAccountProvider(),
|
||||||
update: (context, localeProvider, provider) => provider!..updateProvider(localeProvider),
|
update: (context, localeProvider, provider) => provider!..updateProvider(localeProvider),
|
||||||
),
|
),
|
||||||
ChangeNotifierProxyProvider<AccountProvider, TwoFactorProvider>(
|
ChangeNotifierProxyProvider<AccountProvider, TwoFactorProvider>(
|
||||||
@@ -70,6 +71,7 @@ void main() async {
|
|||||||
update: (context, orgnization, provider) => provider!..update(orgnization),
|
update: (context, orgnization, provider) => provider!..update(orgnization),
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(create: (_) => CarouselIndexProvider()),
|
ChangeNotifierProvider(create: (_) => CarouselIndexProvider()),
|
||||||
|
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (_) => UploadHistoryProvider(service: MockUploadHistoryService())..load(),
|
create: (_) => UploadHistoryProvider(service: MockUploadHistoryService())..load(),
|
||||||
),
|
),
|
||||||
@@ -91,12 +93,16 @@ void main() async {
|
|||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (_) => MockPaymentProvider(),
|
create: (_) => MockPaymentProvider(),
|
||||||
),
|
),
|
||||||
|
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (_) => OperationProvider(OperationService())..loadOperations(),
|
create: (_) => OperationProvider(OperationService())..loadOperations(),
|
||||||
),
|
),
|
||||||
ChangeNotifierProxyProvider<OrganizationsProvider, QuotationProvider>(
|
ChangeNotifierProvider(
|
||||||
|
create: (_) => PaymentAmountProvider(),
|
||||||
|
),
|
||||||
|
ChangeNotifierProxyProvider2<OrganizationsProvider, PaymentAmountProvider, QuotationProvider>(
|
||||||
create: (_) => QuotationProvider(),
|
create: (_) => QuotationProvider(),
|
||||||
update: (context, orgnization, provider) => provider!..update(orgnization),
|
update: (context, orgnization, payment, provider) => provider!..update(orgnization, payment),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const PayApp(),
|
child: const PayApp(),
|
||||||
|
|||||||
1
frontend/pweb/lib/models/edit_state.dart
Normal file
1
frontend/pweb/lib/models/edit_state.dart
Normal file
@@ -0,0 +1 @@
|
|||||||
|
enum EditState { view, edit, saving }
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:pweb/models/currency.dart';
|
import 'package:pshared/models/currency.dart';
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
|
|
||||||
|
|
||||||
class Wallet {
|
class Wallet implements Describable {
|
||||||
final String id;
|
final String id;
|
||||||
final String walletUserID; // ID or number that we show the user
|
final String walletUserID; // ID or number that we show the user
|
||||||
final String name;
|
|
||||||
final double balance;
|
final double balance;
|
||||||
final Currency currency;
|
final Currency currency;
|
||||||
final bool isHidden;
|
final bool isHidden;
|
||||||
@@ -13,14 +13,21 @@ class Wallet {
|
|||||||
final String? network;
|
final String? network;
|
||||||
final String? tokenSymbol;
|
final String? tokenSymbol;
|
||||||
final String? contractAddress;
|
final String? contractAddress;
|
||||||
|
final Describable describable;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => describable.name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get description => describable.description;
|
||||||
|
|
||||||
Wallet({
|
Wallet({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.walletUserID,
|
required this.walletUserID,
|
||||||
required this.name,
|
|
||||||
required this.balance,
|
required this.balance,
|
||||||
required this.currency,
|
required this.currency,
|
||||||
required this.calculatedAt,
|
required this.calculatedAt,
|
||||||
|
required this.describable,
|
||||||
this.isHidden = true,
|
this.isHidden = true,
|
||||||
this.depositAddress,
|
this.depositAddress,
|
||||||
this.network,
|
this.network,
|
||||||
@@ -30,7 +37,6 @@ class Wallet {
|
|||||||
|
|
||||||
Wallet copyWith({
|
Wallet copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
String? name,
|
|
||||||
double? balance,
|
double? balance,
|
||||||
Currency? currency,
|
Currency? currency,
|
||||||
String? walletUserID,
|
String? walletUserID,
|
||||||
@@ -39,9 +45,11 @@ class Wallet {
|
|||||||
String? network,
|
String? network,
|
||||||
String? tokenSymbol,
|
String? tokenSymbol,
|
||||||
String? contractAddress,
|
String? contractAddress,
|
||||||
|
Describable? describable,
|
||||||
|
String? name,
|
||||||
|
String? Function()? description,
|
||||||
}) => Wallet(
|
}) => Wallet(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
name: name ?? this.name,
|
|
||||||
balance: balance ?? this.balance,
|
balance: balance ?? this.balance,
|
||||||
currency: currency ?? this.currency,
|
currency: currency ?? this.currency,
|
||||||
walletUserID: walletUserID ?? this.walletUserID,
|
walletUserID: walletUserID ?? this.walletUserID,
|
||||||
@@ -51,5 +59,9 @@ class Wallet {
|
|||||||
network: network ?? this.network,
|
network: network ?? this.network,
|
||||||
tokenSymbol: tokenSymbol ?? this.tokenSymbol,
|
tokenSymbol: tokenSymbol ?? this.tokenSymbol,
|
||||||
contractAddress: contractAddress ?? this.contractAddress,
|
contractAddress: contractAddress ?? this.contractAddress,
|
||||||
|
describable: describable
|
||||||
|
?? (name != null || description != null
|
||||||
|
? this.describable.copyWith(name: name, description: description)
|
||||||
|
: this.describable),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
|
|
||||||
import 'package:pshared/models/payment/status.dart';
|
import 'package:pshared/models/payment/status.dart';
|
||||||
|
|
||||||
import 'package:pweb/models/currency.dart';
|
import 'package:pshared/models/currency.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:pweb/pages/payment_methods/form.dart';
|
|||||||
import 'package:pweb/pages/payment_methods/icon.dart';
|
import 'package:pweb/pages/payment_methods/icon.dart';
|
||||||
|
|
||||||
|
|
||||||
class AdressBookPaymentMethodTile extends StatefulWidget {
|
class AddressBookPaymentMethodTile extends StatefulWidget {
|
||||||
final PaymentType type;
|
final PaymentType type;
|
||||||
final String title;
|
final String title;
|
||||||
final MethodMap methods;
|
final MethodMap methods;
|
||||||
@@ -18,7 +18,7 @@ class AdressBookPaymentMethodTile extends StatefulWidget {
|
|||||||
final double sizeM;
|
final double sizeM;
|
||||||
final TextStyle? titleTextStyle;
|
final TextStyle? titleTextStyle;
|
||||||
|
|
||||||
const AdressBookPaymentMethodTile({
|
const AddressBookPaymentMethodTile({
|
||||||
super.key,
|
super.key,
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.title,
|
required this.title,
|
||||||
@@ -31,10 +31,10 @@ class AdressBookPaymentMethodTile extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AdressBookPaymentMethodTile> createState() => _AdressBookPaymentMethodTileState();
|
State<AddressBookPaymentMethodTile> createState() => _AddressBookPaymentMethodTileState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AdressBookPaymentMethodTileState extends State<AdressBookPaymentMethodTile> {
|
class _AddressBookPaymentMethodTileState extends State<AddressBookPaymentMethodTile> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user