Compare commits
15 Commits
SEND005
...
da72121109
| Author | SHA1 | Date | |
|---|---|---|---|
| da72121109 | |||
|
|
5bebadf17c | ||
| 1bab0b14ef | |||
|
|
39f323d050 | ||
| 7cd9e14618 | |||
|
|
b77d2c16ab | ||
|
|
324f150950 | ||
| dd6bcf843c | |||
|
|
874cc4971b | ||
| bfe4695b2d | |||
|
|
99161c8e7d | ||
| 6901791dd2 | |||
|
|
acb3d14b47 | ||
| aa5f7e271e | |||
|
|
0a01995f53 |
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger = payload.Intent.GetTrigger()
|
logger = logger.With(logFieldsFromTokenPayload(&payload)...)
|
||||||
|
|
||||||
|
if payload.Intent != nil {
|
||||||
|
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
|
||||||
@@ -288,7 +405,7 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef primitive.Obj
|
|||||||
plan, rule, err := s.resolver.ResolveFeeRule(ctx, orgPtr, convertTrigger(intent.GetTrigger()), bookedAt, intent.GetAttributes())
|
plan, rule, err := s.resolver.ResolveFeeRule(ctx, orgPtr, convertTrigger(intent.GetTrigger()), bookedAt, intent.GetAttributes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, merrors.ErrNoData):
|
case errors.Is(err, merrors.ErrNoData):
|
||||||
return nil, nil, nil, status.Error(codes.NotFound, "fee rule not found")
|
return nil, nil, nil, status.Error(codes.NotFound, "fee rule not found")
|
||||||
case errors.Is(err, merrors.ErrDataConflict):
|
case errors.Is(err, merrors.ErrDataConflict):
|
||||||
return nil, nil, nil, status.Error(codes.FailedPrecondition, "conflicting fee rules")
|
return nil, nil, nil, status.Error(codes.FailedPrecondition, "conflicting fee rules")
|
||||||
@@ -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 (
|
||||||
@@ -25,14 +26,14 @@ 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"`
|
||||||
Rules []FeeRule `bson:"rules,omitempty" json:"rules,omitempty"`
|
Rules []FeeRule `bson:"rules,omitempty" json:"rules,omitempty"`
|
||||||
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
|
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collection implements storable.Storable.
|
// Collection implements storable.Storable.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -383,6 +383,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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -69,7 +71,7 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
|
|||||||
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,12 +41,13 @@ type Service struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type serviceDependencies struct {
|
type serviceDependencies struct {
|
||||||
fees feesDependency
|
fees feesDependency
|
||||||
ledger ledgerDependency
|
ledger ledgerDependency
|
||||||
gateway gatewayDependency
|
gateway gatewayDependency
|
||||||
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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
if m.Currency == "" {
|
type AssetResolver interface {
|
||||||
|
IsSupported(ticker string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, nil); 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, nil); 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ 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 = '';
|
|
||||||
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;
|
||||||
@@ -38,8 +36,6 @@ 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,8 +15,6 @@ 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,8 +21,6 @@ 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;
|
||||||
@@ -42,8 +40,6 @@ 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;
|
||||||
@@ -61,8 +57,6 @@ 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,
|
||||||
|
|||||||
@@ -79,13 +79,12 @@ enum ResourceType {
|
|||||||
@JsonValue('payments')
|
@JsonValue('payments')
|
||||||
payments,
|
payments,
|
||||||
|
|
||||||
/// Represents payment orchestration service
|
|
||||||
@JsonValue('payment_orchestrator')
|
|
||||||
paymentOrchestrator,
|
|
||||||
|
|
||||||
@JsonValue('payment_methods')
|
@JsonValue('payment_methods')
|
||||||
paymentMethods,
|
paymentMethods,
|
||||||
|
|
||||||
|
@JsonValue('payment_orchestrator')
|
||||||
|
paymentOrchestrator,
|
||||||
|
|
||||||
/// Represents permissions service
|
/// Represents permissions service
|
||||||
@JsonValue('permissions')
|
@JsonValue('permissions')
|
||||||
permissions,
|
permissions,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
@@ -22,9 +20,6 @@ import 'package:pshared/utils/exception.dart';
|
|||||||
|
|
||||||
|
|
||||||
class AccountProvider extends ChangeNotifier {
|
class AccountProvider extends ChangeNotifier {
|
||||||
AccountProvider({Future<void> Function(Account?)? onAccountChanged})
|
|
||||||
: _onAccountChanged = onAccountChanged;
|
|
||||||
|
|
||||||
static String get currentUserRef => Constants.nilObjectRef;
|
static String get currentUserRef => Constants.nilObjectRef;
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -32,8 +27,6 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
Resource<Account?> get resource => _resource;
|
Resource<Account?> get resource => _resource;
|
||||||
late LocaleProvider _localeProvider;
|
late LocaleProvider _localeProvider;
|
||||||
PendingLogin? _pendingLogin;
|
PendingLogin? _pendingLogin;
|
||||||
Future<void>? _restoreFuture;
|
|
||||||
Future<void> Function(Account?)? _onAccountChanged;
|
|
||||||
|
|
||||||
Account? get account => _resource.data;
|
Account? get account => _resource.data;
|
||||||
PendingLogin? get pendingLogin => _pendingLogin;
|
PendingLogin? get pendingLogin => _pendingLogin;
|
||||||
@@ -60,21 +53,11 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Private helper to update the resource and notify listeners.
|
// Private helper to update the resource and notify listeners.
|
||||||
void setAccountChangedListener(Future<void> Function(Account?)? listener) => _onAccountChanged = listener;
|
|
||||||
|
|
||||||
void _setResource(Resource<Account?> newResource) {
|
void _setResource(Resource<Account?> newResource) {
|
||||||
final previousAccount = _resource.data;
|
|
||||||
_resource = newResource;
|
_resource = newResource;
|
||||||
_notifyAccountChanged(previousAccount, newResource.data);
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _notifyAccountChanged(Account? previous, Account? current) {
|
|
||||||
if (previous == current) return;
|
|
||||||
final handler = _onAccountChanged;
|
|
||||||
if (handler != null) unawaited(handler(current));
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateProvider(LocaleProvider localeProvider) => _localeProvider = localeProvider;
|
void updateProvider(LocaleProvider localeProvider) => _localeProvider = localeProvider;
|
||||||
|
|
||||||
void _pickupLocale(String locale) => _localeProvider.setLocale(Locale(locale));
|
void _pickupLocale(String locale) => _localeProvider.setLocale(Locale(locale));
|
||||||
@@ -237,11 +220,4 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> restoreIfPossible() {
|
|
||||||
return _restoreFuture ??= AuthorizationService.isAuthorizationStored().then<void>((hasAuth) async {
|
|
||||||
if (!hasAuth) return;
|
|
||||||
await restore();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ 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;
|
||||||
bool _errorHandled = false;
|
|
||||||
|
|
||||||
void update(OrganizationsProvider venue) {
|
void update(OrganizationsProvider venue) {
|
||||||
_organizations = venue;
|
_organizations = venue;
|
||||||
@@ -45,7 +44,6 @@ 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 {
|
||||||
_userAccess = _userAccess.copyWith(isLoading: true, error: null);
|
_userAccess = _userAccess.copyWith(isLoading: true, error: null);
|
||||||
_errorHandled = false;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -69,12 +67,6 @@ class PermissionsProvider extends ChangeNotifier {
|
|||||||
return _userAccess.data;
|
return _userAccess.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasUnhandledError => error != null && !_errorHandled;
|
|
||||||
|
|
||||||
void markErrorHandled() {
|
|
||||||
_errorHandled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<UserAccess?> changeRole(String accountRef, String newRoleDescRef) async {
|
Future<UserAccess?> changeRole(String accountRef, String newRoleDescRef) async {
|
||||||
final currentRole = roles.firstWhereOrNull((r) => r.accountRef == accountRef);
|
final currentRole = roles.firstWhereOrNull((r) => r.accountRef == accountRef);
|
||||||
final currentDesc = currentRole != null
|
final currentDesc = currentRole != null
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ 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';
|
||||||
|
|
||||||
@@ -41,9 +40,11 @@ void _setupLogging() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
|
||||||
await Constants.initialize();
|
await Constants.initialize();
|
||||||
await PosthogService.initialize();
|
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// await AmplitudeService.initialize();
|
||||||
|
|
||||||
|
|
||||||
_setupLogging();
|
_setupLogging();
|
||||||
@@ -56,12 +57,7 @@ void main() async {
|
|||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (_) => LocaleProvider(null)),
|
ChangeNotifierProvider(create: (_) => LocaleProvider(null)),
|
||||||
ChangeNotifierProxyProvider<LocaleProvider, AccountProvider>(
|
ChangeNotifierProxyProvider<LocaleProvider, AccountProvider>(
|
||||||
create: (_) => AccountProvider(
|
create: (_) => AccountProvider(),
|
||||||
onAccountChanged: (account) {
|
|
||||||
if (account == null) return Future<void>.value();
|
|
||||||
return PosthogService.identify(account);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
update: (context, localeProvider, provider) => provider!..updateProvider(localeProvider),
|
update: (context, localeProvider, provider) => provider!..updateProvider(localeProvider),
|
||||||
),
|
),
|
||||||
ChangeNotifierProxyProvider<AccountProvider, TwoFactorProvider>(
|
ChangeNotifierProxyProvider<AccountProvider, TwoFactorProvider>(
|
||||||
@@ -74,7 +70,6 @@ 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(),
|
||||||
),
|
),
|
||||||
@@ -96,7 +91,6 @@ void main() async {
|
|||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (_) => MockPaymentProvider(),
|
create: (_) => MockPaymentProvider(),
|
||||||
),
|
),
|
||||||
|
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (_) => OperationProvider(OperationService())..loadOperations(),
|
create: (_) => OperationProvider(OperationService())..loadOperations(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@@ -16,7 +14,6 @@ import 'package:pshared/provider/recipient/pmethods.dart';
|
|||||||
import 'package:pshared/provider/recipient/provider.dart';
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/address_book/form/view.dart';
|
import 'package:pweb/pages/address_book/form/view.dart';
|
||||||
import 'package:pweb/services/posthog.dart';
|
|
||||||
import 'package:pweb/utils/error/snackbar.dart';
|
import 'package:pweb/utils/error/snackbar.dart';
|
||||||
import 'package:pweb/utils/payment/label.dart';
|
import 'package:pweb/utils/payment/label.dart';
|
||||||
import 'package:pweb/utils/snackbar.dart';
|
import 'package:pweb/utils/snackbar.dart';
|
||||||
@@ -109,11 +106,11 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
unawaited(PosthogService.recipientAddCompleted(
|
// AmplitudeService.recipientAddCompleted(
|
||||||
_type,
|
// _type,
|
||||||
_status,
|
// _status,
|
||||||
_methods.keys.toSet(),
|
// _methods.keys.toSet(),
|
||||||
));
|
// );
|
||||||
final recipient = await executeActionWithNotification(
|
final recipient = await executeActionWithNotification(
|
||||||
context: context,
|
context: context,
|
||||||
action: _doSave,
|
action: _doSave,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -11,50 +10,26 @@ import 'package:pweb/widgets/error/snackbar.dart';
|
|||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
class AccountLoader extends StatefulWidget {
|
class AccountLoader extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const AccountLoader({super.key, required this.child});
|
const AccountLoader({super.key, required this.child});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AccountLoader> createState() => _AccountLoaderState();
|
Widget build(BuildContext context) => Consumer<AccountProvider>(builder: (context, provider, _) {
|
||||||
}
|
if (provider.isLoading) return const Center(child: CircularProgressIndicator());
|
||||||
|
if (provider.error != null) {
|
||||||
class _AccountLoaderState extends State<AccountLoader> {
|
postNotifyUserOfErrorX(
|
||||||
@override
|
context: context,
|
||||||
void initState() {
|
errorSituation: AppLocalizations.of(context)!.errorLogin,
|
||||||
super.initState();
|
exception: provider.error!,
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
);
|
||||||
final provider = Provider.of<AccountProvider>(context, listen: false);
|
navigateAndReplace(context, Pages.login);
|
||||||
if (provider.account == null) {
|
}
|
||||||
provider.restoreIfPossible().catchError((error, stack) {
|
if (provider.account == null) {
|
||||||
Logger('Account restore failed: $error');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Consumer<AccountProvider>(builder: (context, provider, _) {
|
|
||||||
if (provider.account != null) {
|
|
||||||
return widget.child;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider.error != null) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
postNotifyUserOfErrorX(
|
|
||||||
context: context,
|
|
||||||
errorSituation: AppLocalizations.of(context)!.errorLogin,
|
|
||||||
exception: provider.error!,
|
|
||||||
);
|
|
||||||
navigateAndReplace(context, Pages.login);
|
|
||||||
});
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider.isLoading) return const Center(child: CircularProgressIndicator());
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => navigateAndReplace(context, Pages.login));
|
WidgetsBinding.instance.addPostFrameCallback((_) => navigateAndReplace(context, Pages.login));
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
});
|
}
|
||||||
}
|
return child;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,29 +21,20 @@ class PermissionsLoader extends StatelessWidget {
|
|||||||
if (provider.isLoading) {
|
if (provider.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.error != null) {
|
if (provider.error != null) {
|
||||||
if (provider.hasUnhandledError) {
|
postNotifyUserOfErrorX(
|
||||||
provider.markErrorHandled();
|
context: context,
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
errorSituation: AppLocalizations.of(context)!.errorLogin,
|
||||||
postNotifyUserOfErrorX(
|
exception: provider.error!,
|
||||||
context: context,
|
);
|
||||||
errorSituation: AppLocalizations.of(context)!.errorLogin,
|
navigateAndReplace(context, Pages.login);
|
||||||
exception: provider.error!,
|
|
||||||
);
|
|
||||||
navigateAndReplace(context, Pages.login);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.error == null && !provider.isReady && accountProvider.account != null) {
|
if (provider.error == null && !provider.isReady && accountProvider.account != null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
provider.load();
|
provider.load();
|
||||||
});
|
});
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
return child;
|
return child;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -16,7 +14,6 @@ import 'package:pweb/widgets/password/password.dart';
|
|||||||
import 'package:pweb/widgets/username.dart';
|
import 'package:pweb/widgets/username.dart';
|
||||||
import 'package:pweb/widgets/vspacer.dart';
|
import 'package:pweb/widgets/vspacer.dart';
|
||||||
import 'package:pweb/widgets/error/snackbar.dart';
|
import 'package:pweb/widgets/error/snackbar.dart';
|
||||||
import 'package:pweb/services/posthog.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -46,7 +43,6 @@ class _LoginFormState extends State<LoginForm> {
|
|||||||
password: _passwordController.text,
|
password: _passwordController.text,
|
||||||
locale: context.read<LocaleProvider>().locale.languageCode,
|
locale: context.read<LocaleProvider>().locale.languageCode,
|
||||||
);
|
);
|
||||||
unawaited(PosthogService.login(pending: outcome.isPending));
|
|
||||||
if (outcome.isPending) {
|
if (outcome.isPending) {
|
||||||
// TODO: fix context usage
|
// TODO: fix context usage
|
||||||
navigateAndReplace(context, Pages.sfactor);
|
navigateAndReplace(context, Pages.sfactor);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import 'package:pweb/providers/payment_flow.dart';
|
|||||||
import 'package:pweb/pages/payment_methods/payment_page/body.dart';
|
import 'package:pweb/pages/payment_methods/payment_page/body.dart';
|
||||||
import 'package:pweb/providers/wallets.dart';
|
import 'package:pweb/providers/wallets.dart';
|
||||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||||
import 'package:pweb/services/posthog.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentPage extends StatefulWidget {
|
class PaymentPage extends StatefulWidget {
|
||||||
@@ -110,7 +109,7 @@ class _PaymentPageState extends State<PaymentPage> {
|
|||||||
|
|
||||||
void _handleSendPayment() {
|
void _handleSendPayment() {
|
||||||
// TODO: Handle Payment logic
|
// TODO: Handle Payment logic
|
||||||
PosthogService.paymentInitiated(method: _flowProvider.selectedType);
|
// AmplitudeService.paymentInitiated();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -196,4 +195,4 @@ class _PaymentPageState extends State<PaymentPage> {
|
|||||||
(method.description?.contains(wallet.walletUserID) ?? false),
|
(method.description?.contains(wallet.walletUserID) ?? false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:pshared/provider/locale.dart';
|
import 'package:pshared/provider/locale.dart';
|
||||||
|
|
||||||
import 'package:pweb/services/posthog.dart';
|
// import 'package:pweb/services/amplitude.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class LocalePicker extends StatelessWidget {
|
|||||||
onChanged: (locale) {
|
onChanged: (locale) {
|
||||||
if (locale != null) {
|
if (locale != null) {
|
||||||
localeProvider.setLocale(locale);
|
localeProvider.setLocale(locale);
|
||||||
PosthogService.localeChanged(locale);
|
// AmplitudeService.localeChanged(locale);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
import 'package:posthog_flutter/posthog_flutter.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/config/constants.dart';
|
|
||||||
import 'package:pshared/models/account/account.dart';
|
|
||||||
import 'package:pshared/models/payment/type.dart';
|
|
||||||
import 'package:pshared/models/recipient/status.dart';
|
|
||||||
import 'package:pshared/models/recipient/type.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class PosthogService {
|
|
||||||
static final _logger = Logger('service.posthog');
|
|
||||||
static String? _identifiedUserId;
|
|
||||||
static bool _initialized = false;
|
|
||||||
|
|
||||||
static bool get isEnabled => _initialized;
|
|
||||||
|
|
||||||
static Future<void> initialize() async {
|
|
||||||
final apiKey = Constants.posthogApiKey;
|
|
||||||
if (apiKey.isEmpty) {
|
|
||||||
_logger.warning('PostHog API key is not configured, analytics disabled.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final config = PostHogConfig(apiKey)
|
|
||||||
..host = Constants.posthogHost
|
|
||||||
..captureApplicationLifecycleEvents = true;
|
|
||||||
await Posthog().setup(config);
|
|
||||||
await Posthog().register('client_id', Constants.clientId);
|
|
||||||
_initialized = true;
|
|
||||||
_logger.info('PostHog initialized with host ${Constants.posthogHost}');
|
|
||||||
} catch (e, st) {
|
|
||||||
_initialized = false;
|
|
||||||
_logger.warning('Failed to initialize PostHog: $e', e, st);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> identify(Account account) async {
|
|
||||||
if (!_initialized) return;
|
|
||||||
if (_identifiedUserId == account.id) return;
|
|
||||||
|
|
||||||
await Posthog().identify(
|
|
||||||
userId: account.id,
|
|
||||||
userProperties: {
|
|
||||||
'email': account.login,
|
|
||||||
'name': account.name,
|
|
||||||
'locale': account.locale,
|
|
||||||
'created_at': account.createdAt.toIso8601String(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
_identifiedUserId = account.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> reset() async {
|
|
||||||
if (!_initialized) return;
|
|
||||||
_identifiedUserId = null;
|
|
||||||
await Posthog().reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> login({required bool pending}) async {
|
|
||||||
if (!_initialized) return;
|
|
||||||
await _capture(
|
|
||||||
'login',
|
|
||||||
properties: {
|
|
||||||
'result': pending ? 'pending' : 'success',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> pageOpened(PayoutDestination page, {String? path, String? uiSource}) async {
|
|
||||||
if (!_initialized) return;
|
|
||||||
return _capture(
|
|
||||||
'pageOpened',
|
|
||||||
properties: {
|
|
||||||
'page': page.name,
|
|
||||||
if (path != null) 'path': path,
|
|
||||||
if (uiSource != null) 'uiSource': uiSource,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> localeChanged(Locale locale) async {
|
|
||||||
if (!_initialized) return;
|
|
||||||
return _capture(
|
|
||||||
'localeChanged',
|
|
||||||
properties: {'locale': locale.toLanguageTag()},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> recipientAddCompleted(
|
|
||||||
RecipientType type,
|
|
||||||
RecipientStatus status,
|
|
||||||
Set<PaymentType> methods,
|
|
||||||
) async {
|
|
||||||
if (!_initialized) return;
|
|
||||||
return _capture(
|
|
||||||
'recipientAddCompleted',
|
|
||||||
properties: {
|
|
||||||
'methods': methods.map((m) => m.name).toList(),
|
|
||||||
'type': type.name,
|
|
||||||
'status': status.name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> paymentInitiated({PaymentType? method}) async {
|
|
||||||
if (!_initialized) return;
|
|
||||||
return _capture(
|
|
||||||
'paymentInitiated',
|
|
||||||
properties: {
|
|
||||||
if (method != null) 'method': method.name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> _capture(
|
|
||||||
String eventName, {
|
|
||||||
Map<String, Object?>? properties,
|
|
||||||
}) async {
|
|
||||||
if (!_initialized) return;
|
|
||||||
final filtered = <String, Object>{};
|
|
||||||
if (properties != null) {
|
|
||||||
for (final entry in properties.entries) {
|
|
||||||
final value = entry.value;
|
|
||||||
if (value != null) filtered[entry.key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Posthog().capture(eventName: eventName, properties: filtered.isEmpty ? null : filtered);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,12 +6,10 @@ import 'package:pshared/provider/account.dart';
|
|||||||
import 'package:pshared/provider/permissions.dart';
|
import 'package:pshared/provider/permissions.dart';
|
||||||
|
|
||||||
import 'package:pweb/app/router/pages.dart';
|
import 'package:pweb/app/router/pages.dart';
|
||||||
import 'package:pweb/services/posthog.dart';
|
|
||||||
|
|
||||||
|
|
||||||
void logoutUtil(BuildContext context) {
|
void logoutUtil(BuildContext context) {
|
||||||
context.read<AccountProvider>().logout();
|
context.read<AccountProvider>().logout();
|
||||||
context.read<PermissionsProvider>().reset();
|
context.read<PermissionsProvider>().reset();
|
||||||
PosthogService.reset();
|
|
||||||
navigateAndReplace(context, Pages.login);
|
navigateAndReplace(context, Pages.login);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:pweb/services/posthog.dart';
|
// import 'package:pweb/services/amplitude.dart';
|
||||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ class SideMenuColumn extends StatelessWidget {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onSelected(item);
|
onSelected(item);
|
||||||
PosthogService.pageOpened(item, uiSource: 'sidebar');
|
// AmplitudeService.pageOpened(item, uiSource: 'sidebar');
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
hoverColor: theme.colorScheme.primaryContainer,
|
hoverColor: theme.colorScheme.primaryContainer,
|
||||||
@@ -76,4 +76,4 @@ class SideMenuColumn extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,6 @@ import amplitude_flutter
|
|||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import flutter_timezone
|
import flutter_timezone
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import posthog_flutter
|
|
||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
@@ -20,7 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
PosthogFlutterPlugin.register(with: registry.registrar(forPlugin: "PosthogFlutterPlugin"))
|
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
pshared:
|
pshared:
|
||||||
path: ../pshared
|
path: ../pshared
|
||||||
posthog_flutter: ^5.9.0
|
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
|||||||
Reference in New Issue
Block a user