Compare commits
13 Commits
d6a3a0cc5b
...
SEND066
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97b16542c2 | ||
|
|
39c04beb21 | ||
| 15393765b9 | |||
|
|
440b6a2553 | ||
| bc76cfe063 | |||
|
|
ed8f7c519c | ||
|
|
71d99338f2 | ||
| b499778bce | |||
|
|
4a554833c4 | ||
| b7ea11a62b | |||
|
|
026f698d9b | ||
| 0da6078468 | |||
|
|
3b65a2dc3a |
@@ -45,9 +45,6 @@ gateway:
|
||||
treasury:
|
||||
execution_delay: 60s
|
||||
poll_interval: 60s
|
||||
telegram:
|
||||
allowed_chats: []
|
||||
users: []
|
||||
ledger:
|
||||
timeout: 5s
|
||||
limits:
|
||||
|
||||
@@ -50,10 +50,3 @@ treasury:
|
||||
limits:
|
||||
max_amount_per_operation: ""
|
||||
max_daily_amount: ""
|
||||
telegram:
|
||||
allowed_chats: []
|
||||
users:
|
||||
- telegram_user_id: "8273799472"
|
||||
ledger_account: "6972c738949b91ea0395e5fb"
|
||||
- telegram_user_id: "8273507566"
|
||||
ledger_account: "6995d6c118bca1d8baa5f2be"
|
||||
|
||||
@@ -3,7 +3,6 @@ package serverimp
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/gateway/tgsettle/internal/service/gateway"
|
||||
@@ -38,8 +37,6 @@ type config struct {
|
||||
*grpcapp.Config `yaml:",inline"`
|
||||
Gateway gatewayConfig `yaml:"gateway"`
|
||||
Treasury treasuryConfig `yaml:"treasury"`
|
||||
Ledger ledgerConfig `yaml:"ledger"` // deprecated: use treasury.ledger
|
||||
Telegram telegramConfig `yaml:"telegram"` // deprecated: use treasury.telegram
|
||||
}
|
||||
|
||||
type gatewayConfig struct {
|
||||
@@ -50,20 +47,9 @@ type gatewayConfig struct {
|
||||
SuccessReaction string `yaml:"success_reaction"`
|
||||
}
|
||||
|
||||
type telegramConfig struct {
|
||||
AllowedChats []string `yaml:"allowed_chats"`
|
||||
Users []telegramUserConfig `yaml:"users"`
|
||||
}
|
||||
|
||||
type telegramUserConfig struct {
|
||||
TelegramUserID string `yaml:"telegram_user_id"`
|
||||
LedgerAccount string `yaml:"ledger_account"`
|
||||
}
|
||||
|
||||
type treasuryConfig struct {
|
||||
ExecutionDelay time.Duration `yaml:"execution_delay"`
|
||||
PollInterval time.Duration `yaml:"poll_interval"`
|
||||
Telegram telegramConfig `yaml:"telegram"`
|
||||
Ledger ledgerConfig `yaml:"ledger"`
|
||||
Limits treasuryLimitsConfig `yaml:"limits"`
|
||||
}
|
||||
@@ -145,8 +131,6 @@ func (i *Imp) Start() error {
|
||||
if cfg.Messaging != nil {
|
||||
msgSettings = cfg.Messaging.Settings
|
||||
}
|
||||
treasuryTelegram := treasuryTelegramConfig(cfg, i.logger)
|
||||
treasuryLedger := treasuryLedgerConfig(cfg, i.logger)
|
||||
gwCfg := gateway.Config{
|
||||
Rail: cfg.Gateway.Rail,
|
||||
TargetChatIDEnv: cfg.Gateway.TargetChatIDEnv,
|
||||
@@ -159,12 +143,8 @@ func (i *Imp) Start() error {
|
||||
Treasury: gateway.TreasuryConfig{
|
||||
ExecutionDelay: cfg.Treasury.ExecutionDelay,
|
||||
PollInterval: cfg.Treasury.PollInterval,
|
||||
Telegram: gateway.TelegramConfig{
|
||||
AllowedChats: treasuryTelegram.AllowedChats,
|
||||
Users: telegramUsers(treasuryTelegram.Users),
|
||||
},
|
||||
Ledger: gateway.LedgerConfig{
|
||||
Timeout: treasuryLedger.Timeout,
|
||||
Timeout: cfg.Treasury.Ledger.Timeout,
|
||||
},
|
||||
Limits: gateway.TreasuryLimitsConfig{
|
||||
MaxAmountPerOperation: cfg.Treasury.Limits.MaxAmountPerOperation,
|
||||
@@ -228,46 +208,3 @@ func (i *Imp) loadConfig() (*config, error) {
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func telegramUsers(input []telegramUserConfig) []gateway.TelegramUserBinding {
|
||||
result := make([]gateway.TelegramUserBinding, 0, len(input))
|
||||
for _, next := range input {
|
||||
result = append(result, gateway.TelegramUserBinding{
|
||||
TelegramUserID: strings.TrimSpace(next.TelegramUserID),
|
||||
LedgerAccount: strings.TrimSpace(next.LedgerAccount),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func treasuryTelegramConfig(cfg *config, logger mlogger.Logger) telegramConfig {
|
||||
if cfg == nil {
|
||||
return telegramConfig{}
|
||||
}
|
||||
if len(cfg.Treasury.Telegram.Users) > 0 || len(cfg.Treasury.Telegram.AllowedChats) > 0 {
|
||||
return cfg.Treasury.Telegram
|
||||
}
|
||||
if len(cfg.Telegram.Users) > 0 || len(cfg.Telegram.AllowedChats) > 0 {
|
||||
if logger != nil {
|
||||
logger.Warn("Deprecated config path used: telegram.*; move these settings to treasury.telegram.*")
|
||||
}
|
||||
return cfg.Telegram
|
||||
}
|
||||
return cfg.Treasury.Telegram
|
||||
}
|
||||
|
||||
func treasuryLedgerConfig(cfg *config, logger mlogger.Logger) ledgerConfig {
|
||||
if cfg == nil {
|
||||
return ledgerConfig{}
|
||||
}
|
||||
if cfg.Treasury.Ledger.Timeout > 0 {
|
||||
return cfg.Treasury.Ledger
|
||||
}
|
||||
if cfg.Ledger.Timeout > 0 {
|
||||
if logger != nil {
|
||||
logger.Warn("Deprecated config path used: ledger.*; move these settings to treasury.ledger.*")
|
||||
}
|
||||
return cfg.Ledger
|
||||
}
|
||||
return cfg.Treasury.Ledger
|
||||
}
|
||||
|
||||
@@ -68,20 +68,9 @@ type Config struct {
|
||||
Treasury TreasuryConfig
|
||||
}
|
||||
|
||||
type TelegramConfig struct {
|
||||
AllowedChats []string
|
||||
Users []TelegramUserBinding
|
||||
}
|
||||
|
||||
type TelegramUserBinding struct {
|
||||
TelegramUserID string
|
||||
LedgerAccount string
|
||||
}
|
||||
|
||||
type TreasuryConfig struct {
|
||||
ExecutionDelay time.Duration
|
||||
PollInterval time.Duration
|
||||
Telegram TelegramConfig
|
||||
Ledger LedgerConfig
|
||||
Limits TreasuryLimitsConfig
|
||||
}
|
||||
@@ -181,39 +170,13 @@ func (s *Service) Shutdown() {
|
||||
}
|
||||
|
||||
func (s *Service) startTreasuryModule() {
|
||||
if s == nil || s.repo == nil || s.repo.TreasuryRequests() == nil {
|
||||
if s == nil || s.repo == nil || s.repo.TreasuryRequests() == nil || s.repo.TreasuryTelegramUsers() == nil {
|
||||
return
|
||||
}
|
||||
if s.cfg.DiscoveryRegistry == nil {
|
||||
s.logger.Warn("Treasury module disabled: discovery registry is unavailable")
|
||||
return
|
||||
}
|
||||
configuredUsers := s.cfg.Treasury.Telegram.Users
|
||||
if len(configuredUsers) == 0 {
|
||||
return
|
||||
}
|
||||
users := make([]treasurysvc.UserBinding, 0, len(configuredUsers))
|
||||
configuredUserIDs := make([]string, 0, len(configuredUsers))
|
||||
for _, binding := range configuredUsers {
|
||||
userID := strings.TrimSpace(binding.TelegramUserID)
|
||||
accountID := strings.TrimSpace(binding.LedgerAccount)
|
||||
if userID != "" {
|
||||
configuredUserIDs = append(configuredUserIDs, userID)
|
||||
}
|
||||
if userID == "" || accountID == "" {
|
||||
continue
|
||||
}
|
||||
users = append(users, treasurysvc.UserBinding{
|
||||
TelegramUserID: userID,
|
||||
LedgerAccount: accountID,
|
||||
})
|
||||
}
|
||||
if len(users) == 0 {
|
||||
s.logger.Warn("Treasury module disabled: no valid treasury.telegram.users bindings",
|
||||
zap.Int("configured_bindings", len(configuredUsers)),
|
||||
zap.Strings("configured_user_ids", configuredUserIDs))
|
||||
return
|
||||
}
|
||||
|
||||
ledgerTimeout := s.cfg.Treasury.Ledger.Timeout
|
||||
if ledgerTimeout <= 0 {
|
||||
@@ -241,10 +204,9 @@ func (s *Service) startTreasuryModule() {
|
||||
module, err := treasurysvc.NewModule(
|
||||
s.logger,
|
||||
s.repo.TreasuryRequests(),
|
||||
s.repo.TreasuryTelegramUsers(),
|
||||
ledgerClient,
|
||||
treasurysvc.Config{
|
||||
AllowedChats: s.cfg.Treasury.Telegram.AllowedChats,
|
||||
Users: users,
|
||||
ExecutionDelay: executionDelay,
|
||||
PollInterval: pollInterval,
|
||||
MaxAmountPerOperation: s.cfg.Treasury.Limits.MaxAmountPerOperation,
|
||||
|
||||
@@ -81,6 +81,7 @@ type fakeRepo struct {
|
||||
tg *fakeTelegramStore
|
||||
pending *fakePendingStore
|
||||
treasury storage.TreasuryRequestsStore
|
||||
users storage.TreasuryTelegramUsersStore
|
||||
}
|
||||
|
||||
func (f *fakeRepo) Payments() storage.PaymentsStore {
|
||||
@@ -99,6 +100,10 @@ func (f *fakeRepo) TreasuryRequests() storage.TreasuryRequestsStore {
|
||||
return f.treasury
|
||||
}
|
||||
|
||||
func (f *fakeRepo) TreasuryTelegramUsers() storage.TreasuryTelegramUsersStore {
|
||||
return f.users
|
||||
}
|
||||
|
||||
type fakePendingStore struct {
|
||||
mu sync.Mutex
|
||||
records map[string]*storagemodel.PendingConfirmation
|
||||
|
||||
@@ -14,5 +14,5 @@ func markdownCode(value string) string {
|
||||
}
|
||||
|
||||
func markdownCommand(command Command) string {
|
||||
return markdownCode(command.Slash())
|
||||
return command.Slash()
|
||||
}
|
||||
|
||||
@@ -51,6 +51,16 @@ type TreasuryService interface {
|
||||
CancelRequest(ctx context.Context, requestID string, telegramUserID string) (*storagemodel.TreasuryRequest, error)
|
||||
}
|
||||
|
||||
type UserBinding struct {
|
||||
TelegramUserID string
|
||||
LedgerAccountID string
|
||||
AllowedChatIDs []string
|
||||
}
|
||||
|
||||
type UserBindingResolver interface {
|
||||
ResolveUserBinding(ctx context.Context, telegramUserID string) (*UserBinding, error)
|
||||
}
|
||||
|
||||
type limitError interface {
|
||||
error
|
||||
LimitKind() string
|
||||
@@ -65,9 +75,7 @@ type Router struct {
|
||||
send SendTextFunc
|
||||
tracker ScheduleTracker
|
||||
|
||||
allowedChats map[string]struct{}
|
||||
userAccounts map[string]string
|
||||
allowAnyChat bool
|
||||
users UserBindingResolver
|
||||
}
|
||||
|
||||
func NewRouter(
|
||||
@@ -75,43 +83,23 @@ func NewRouter(
|
||||
service TreasuryService,
|
||||
send SendTextFunc,
|
||||
tracker ScheduleTracker,
|
||||
allowedChats []string,
|
||||
userAccounts map[string]string,
|
||||
users UserBindingResolver,
|
||||
) *Router {
|
||||
if logger != nil {
|
||||
logger = logger.Named("treasury_router")
|
||||
}
|
||||
allowed := map[string]struct{}{}
|
||||
for _, chatID := range allowedChats {
|
||||
chatID = strings.TrimSpace(chatID)
|
||||
if chatID == "" {
|
||||
continue
|
||||
}
|
||||
allowed[chatID] = struct{}{}
|
||||
}
|
||||
users := map[string]string{}
|
||||
for userID, accountID := range userAccounts {
|
||||
userID = strings.TrimSpace(userID)
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if userID == "" || accountID == "" {
|
||||
continue
|
||||
}
|
||||
users[userID] = accountID
|
||||
}
|
||||
return &Router{
|
||||
logger: logger,
|
||||
service: service,
|
||||
dialogs: NewDialogs(),
|
||||
send: send,
|
||||
tracker: tracker,
|
||||
allowedChats: allowed,
|
||||
userAccounts: users,
|
||||
allowAnyChat: len(allowed) == 0,
|
||||
users: users,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) Enabled() bool {
|
||||
return r != nil && r.service != nil && len(r.userAccounts) > 0
|
||||
return r != nil && r.service != nil && r.users != nil
|
||||
}
|
||||
|
||||
func (r *Router) HandleUpdate(ctx context.Context, update *model.TelegramWebhookUpdate) bool {
|
||||
@@ -138,20 +126,28 @@ func (r *Router) HandleUpdate(ctx context.Context, update *model.TelegramWebhook
|
||||
)
|
||||
}
|
||||
|
||||
if !r.allowAnyChat {
|
||||
if _, ok := r.allowedChats[chatID]; !ok {
|
||||
r.logUnauthorized(update)
|
||||
_ = r.sendText(ctx, chatID, unauthorizedChatMessage)
|
||||
binding, err := r.users.ResolveUserBinding(ctx, userID)
|
||||
if err != nil {
|
||||
if r.logger != nil {
|
||||
r.logger.Warn("Failed to resolve treasury user binding",
|
||||
zap.Error(err),
|
||||
zap.String("telegram_user_id", userID),
|
||||
zap.String("chat_id", chatID))
|
||||
}
|
||||
_ = r.sendText(ctx, chatID, "*Temporary issue*\nUnable to check treasury authorization right now. Please try again.")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
accountID, ok := r.userAccounts[userID]
|
||||
if !ok || strings.TrimSpace(accountID) == "" {
|
||||
if binding == nil || strings.TrimSpace(binding.LedgerAccountID) == "" {
|
||||
r.logUnauthorized(update)
|
||||
_ = r.sendText(ctx, chatID, unauthorizedMessage)
|
||||
return true
|
||||
}
|
||||
if !isChatAllowed(chatID, binding.AllowedChatIDs) {
|
||||
r.logUnauthorized(update)
|
||||
_ = r.sendText(ctx, chatID, unauthorizedChatMessage)
|
||||
return true
|
||||
}
|
||||
accountID := strings.TrimSpace(binding.LedgerAccountID)
|
||||
|
||||
switch command {
|
||||
case CommandStart:
|
||||
@@ -507,6 +503,22 @@ func (r *Router) resolveAccountProfile(ctx context.Context, ledgerAccountID stri
|
||||
return profile
|
||||
}
|
||||
|
||||
func isChatAllowed(chatID string, allowedChatIDs []string) bool {
|
||||
chatID = strings.TrimSpace(chatID)
|
||||
if chatID == "" {
|
||||
return false
|
||||
}
|
||||
if len(allowedChatIDs) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, allowed := range allowedChatIDs {
|
||||
if strings.TrimSpace(allowed) == chatID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func formatSeconds(value int64) string {
|
||||
if value == 1 {
|
||||
return "1 second"
|
||||
|
||||
@@ -12,6 +12,21 @@ import (
|
||||
|
||||
type fakeService struct{}
|
||||
|
||||
type fakeUserBindingResolver struct {
|
||||
bindings map[string]*UserBinding
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeUserBindingResolver) ResolveUserBinding(_ context.Context, telegramUserID string) (*UserBinding, error) {
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
if f.bindings == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return f.bindings[telegramUserID], nil
|
||||
}
|
||||
|
||||
func (fakeService) ExecutionDelay() time.Duration {
|
||||
return 30 * time.Second
|
||||
}
|
||||
@@ -54,8 +69,15 @@ func TestRouterUnauthorizedInAllowedChatSendsAccessDenied(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
[]string{"100"},
|
||||
map[string]string{"123": "acct-1"},
|
||||
fakeUserBindingResolver{
|
||||
bindings: map[string]*UserBinding{
|
||||
"123": {
|
||||
TelegramUserID: "123",
|
||||
LedgerAccountID: "acct-1",
|
||||
AllowedChatIDs: []string{"100"},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
handled := router.HandleUpdate(context.Background(), &model.TelegramWebhookUpdate{
|
||||
Message: &model.TelegramMessage{
|
||||
@@ -85,8 +107,15 @@ func TestRouterUnknownChatGetsDenied(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
[]string{"100"},
|
||||
map[string]string{"123": "acct-1"},
|
||||
fakeUserBindingResolver{
|
||||
bindings: map[string]*UserBinding{
|
||||
"123": {
|
||||
TelegramUserID: "123",
|
||||
LedgerAccountID: "acct-1",
|
||||
AllowedChatIDs: []string{"100"},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
handled := router.HandleUpdate(context.Background(), &model.TelegramWebhookUpdate{
|
||||
Message: &model.TelegramMessage{
|
||||
@@ -116,8 +145,14 @@ func TestRouterEmptyAllowedChats_AllowsAnyChatForAuthorizedUser(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
map[string]string{"123": "acct-1"},
|
||||
fakeUserBindingResolver{
|
||||
bindings: map[string]*UserBinding{
|
||||
"123": {
|
||||
TelegramUserID: "123",
|
||||
LedgerAccountID: "acct-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
handled := router.HandleUpdate(context.Background(), &model.TelegramWebhookUpdate{
|
||||
Message: &model.TelegramMessage{
|
||||
@@ -151,8 +186,14 @@ func TestRouterEmptyAllowedChats_UnauthorizedUserGetsDenied(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
map[string]string{"123": "acct-1"},
|
||||
fakeUserBindingResolver{
|
||||
bindings: map[string]*UserBinding{
|
||||
"123": {
|
||||
TelegramUserID: "123",
|
||||
LedgerAccountID: "acct-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
handled := router.HandleUpdate(context.Background(), &model.TelegramWebhookUpdate{
|
||||
Message: &model.TelegramMessage{
|
||||
@@ -182,8 +223,14 @@ func TestRouterStartAuthorizedShowsWelcome(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
map[string]string{"123": "acct-1"},
|
||||
fakeUserBindingResolver{
|
||||
bindings: map[string]*UserBinding{
|
||||
"123": {
|
||||
TelegramUserID: "123",
|
||||
LedgerAccountID: "acct-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
handled := router.HandleUpdate(context.Background(), &model.TelegramWebhookUpdate{
|
||||
Message: &model.TelegramMessage{
|
||||
@@ -213,8 +260,14 @@ func TestRouterHelpAuthorizedShowsHelp(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
map[string]string{"123": "acct-1"},
|
||||
fakeUserBindingResolver{
|
||||
bindings: map[string]*UserBinding{
|
||||
"123": {
|
||||
TelegramUserID: "123",
|
||||
LedgerAccountID: "acct-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
handled := router.HandleUpdate(context.Background(), &model.TelegramWebhookUpdate{
|
||||
Message: &model.TelegramMessage{
|
||||
@@ -244,8 +297,14 @@ func TestRouterStartUnauthorizedGetsDenied(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
map[string]string{"123": "acct-1"},
|
||||
fakeUserBindingResolver{
|
||||
bindings: map[string]*UserBinding{
|
||||
"123": {
|
||||
TelegramUserID: "123",
|
||||
LedgerAccountID: "acct-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
handled := router.HandleUpdate(context.Background(), &model.TelegramWebhookUpdate{
|
||||
Message: &model.TelegramMessage{
|
||||
@@ -275,8 +334,14 @@ func TestRouterPlainTextWithoutSession_ShowsSupportedCommands(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
map[string]string{"123": "acct-1"},
|
||||
fakeUserBindingResolver{
|
||||
bindings: map[string]*UserBinding{
|
||||
"123": {
|
||||
TelegramUserID: "123",
|
||||
LedgerAccountID: "acct-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
handled := router.HandleUpdate(context.Background(), &model.TelegramWebhookUpdate{
|
||||
Message: &model.TelegramMessage{
|
||||
|
||||
@@ -2,15 +2,7 @@ package treasury
|
||||
|
||||
import "time"
|
||||
|
||||
type UserBinding struct {
|
||||
TelegramUserID string
|
||||
LedgerAccount string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
AllowedChats []string
|
||||
Users []UserBinding
|
||||
|
||||
ExecutionDelay time.Duration
|
||||
PollInterval time.Duration
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ type Module struct {
|
||||
func NewModule(
|
||||
logger mlogger.Logger,
|
||||
repo storage.TreasuryRequestsStore,
|
||||
users storage.TreasuryTelegramUsersStore,
|
||||
ledgerClient ledger.Client,
|
||||
cfg Config,
|
||||
send bot.SendTextFunc,
|
||||
@@ -33,6 +34,9 @@ func NewModule(
|
||||
if logger != nil {
|
||||
logger = logger.Named("treasury")
|
||||
}
|
||||
if users == nil {
|
||||
return nil, merrors.InvalidArgument("treasury telegram users store is required", "users")
|
||||
}
|
||||
service, err := NewService(
|
||||
logger,
|
||||
repo,
|
||||
@@ -45,23 +49,13 @@ func NewModule(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := map[string]string{}
|
||||
for _, binding := range cfg.Users {
|
||||
userID := strings.TrimSpace(binding.TelegramUserID)
|
||||
accountID := strings.TrimSpace(binding.LedgerAccount)
|
||||
if userID == "" || accountID == "" {
|
||||
continue
|
||||
}
|
||||
users[userID] = accountID
|
||||
}
|
||||
|
||||
module := &Module{
|
||||
logger: logger,
|
||||
service: service,
|
||||
ledger: ledgerClient,
|
||||
}
|
||||
module.scheduler = NewScheduler(logger, service, NotifyFunc(send), cfg.PollInterval)
|
||||
module.router = bot.NewRouter(logger, &botServiceAdapter{svc: service}, send, module.scheduler, cfg.AllowedChats, users)
|
||||
module.router = bot.NewRouter(logger, &botServiceAdapter{svc: service}, send, module.scheduler, &botUsersAdapter{store: users})
|
||||
return module, nil
|
||||
}
|
||||
|
||||
@@ -99,6 +93,28 @@ type botServiceAdapter struct {
|
||||
svc *Service
|
||||
}
|
||||
|
||||
type botUsersAdapter struct {
|
||||
store storage.TreasuryTelegramUsersStore
|
||||
}
|
||||
|
||||
func (a *botUsersAdapter) ResolveUserBinding(ctx context.Context, telegramUserID string) (*bot.UserBinding, error) {
|
||||
if a == nil || a.store == nil {
|
||||
return nil, merrors.Internal("treasury users store unavailable")
|
||||
}
|
||||
record, err := a.store.FindByTelegramUserID(ctx, telegramUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &bot.UserBinding{
|
||||
TelegramUserID: strings.TrimSpace(record.TelegramUserID),
|
||||
LedgerAccountID: strings.TrimSpace(record.LedgerAccountID),
|
||||
AllowedChatIDs: normalizeChatIDs(record.AllowedChatIDs),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *botServiceAdapter) ExecutionDelay() (delay time.Duration) {
|
||||
if a == nil || a.svc == nil {
|
||||
return 0
|
||||
@@ -164,3 +180,26 @@ func (a *botServiceAdapter) CancelRequest(ctx context.Context, requestID string,
|
||||
}
|
||||
return a.svc.CancelRequest(ctx, requestID, telegramUserID)
|
||||
}
|
||||
|
||||
func normalizeChatIDs(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(values))
|
||||
seen := map[string]struct{}{}
|
||||
for _, next := range values {
|
||||
next = strings.TrimSpace(next)
|
||||
if next == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[next]; ok {
|
||||
continue
|
||||
}
|
||||
seen[next] = struct{}{}
|
||||
out = append(out, next)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -441,7 +441,7 @@ func (s *Service) logRequest(record *storagemodel.TreasuryRequest, status string
|
||||
}
|
||||
|
||||
func newRequestID() string {
|
||||
return "TGSETTLE-" + strings.ToUpper(bson.NewObjectID().Hex()[:8])
|
||||
return "TG-TREASURY-" + strings.ToUpper(bson.NewObjectID().Hex())
|
||||
}
|
||||
|
||||
func resolveAccountCode(account *ledger.Account, fallbackAccountID string) string {
|
||||
|
||||
@@ -5,6 +5,7 @@ const (
|
||||
telegramConfirmationsCollection = "telegram_confirmations"
|
||||
pendingConfirmationsCollection = "pending_confirmations"
|
||||
treasuryRequestsCollection = "treasury_requests"
|
||||
treasuryTelegramUsersCollection = "treasury_telegram_users"
|
||||
)
|
||||
|
||||
func (*PaymentRecord) Collection() string {
|
||||
@@ -22,3 +23,7 @@ func (*PendingConfirmation) Collection() string {
|
||||
func (*TreasuryRequest) Collection() string {
|
||||
return treasuryRequestsCollection
|
||||
}
|
||||
|
||||
func (*TreasuryTelegramUser) Collection() string {
|
||||
return treasuryTelegramUsersCollection
|
||||
}
|
||||
|
||||
@@ -49,3 +49,11 @@ type TreasuryRequest struct {
|
||||
|
||||
Active bool `bson:"active,omitempty" json:"active,omitempty"`
|
||||
}
|
||||
|
||||
type TreasuryTelegramUser struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
|
||||
TelegramUserID string `bson:"telegramUserId,omitempty" json:"telegram_user_id,omitempty"`
|
||||
LedgerAccountID string `bson:"ledgerAccountId,omitempty" json:"ledger_account_id,omitempty"`
|
||||
AllowedChatIDs []string `bson:"allowedChatIds,omitempty" json:"allowed_chat_ids,omitempty"`
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ type Repository struct {
|
||||
tg storage.TelegramConfirmationsStore
|
||||
pending storage.PendingConfirmationsStore
|
||||
treasury storage.TreasuryRequestsStore
|
||||
users storage.TreasuryTelegramUsersStore
|
||||
outbox gatewayoutbox.Store
|
||||
}
|
||||
|
||||
@@ -80,6 +81,11 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) {
|
||||
result.logger.Error("Failed to initialise treasury requests store", zap.Error(err), zap.String("store", "treasury_requests"))
|
||||
return nil, err
|
||||
}
|
||||
treasuryUsersStore, err := store.NewTreasuryTelegramUsers(result.logger, result.db)
|
||||
if err != nil {
|
||||
result.logger.Error("Failed to initialise treasury telegram users store", zap.Error(err), zap.String("store", "treasury_telegram_users"))
|
||||
return nil, err
|
||||
}
|
||||
outboxStore, err := gatewayoutbox.NewMongoStore(result.logger, result.db)
|
||||
if err != nil {
|
||||
result.logger.Error("Failed to initialise outbox store", zap.Error(err), zap.String("store", "outbox"))
|
||||
@@ -89,6 +95,7 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) {
|
||||
result.tg = tgStore
|
||||
result.pending = pendingStore
|
||||
result.treasury = treasuryStore
|
||||
result.users = treasuryUsersStore
|
||||
result.outbox = outboxStore
|
||||
result.logger.Info("Payment gateway MongoDB storage initialised")
|
||||
return result, nil
|
||||
@@ -110,6 +117,10 @@ func (r *Repository) TreasuryRequests() storage.TreasuryRequestsStore {
|
||||
return r.treasury
|
||||
}
|
||||
|
||||
func (r *Repository) TreasuryTelegramUsers() storage.TreasuryTelegramUsersStore {
|
||||
return r.users
|
||||
}
|
||||
|
||||
func (r *Repository) Outbox() gatewayoutbox.Store {
|
||||
return r.outbox
|
||||
}
|
||||
|
||||
@@ -296,20 +296,48 @@ func (t *TreasuryRequests) Update(ctx context.Context, record *model.TreasuryReq
|
||||
Set(repository.Field("operationType"), record.OperationType).
|
||||
Set(repository.Field("telegramUserId"), record.TelegramUserID).
|
||||
Set(repository.Field("ledgerAccountId"), record.LedgerAccountID).
|
||||
Set(repository.Field("ledgerAccountCode"), record.LedgerAccountCode).
|
||||
Set(repository.Field("organizationRef"), record.OrganizationRef).
|
||||
Set(repository.Field("chatId"), record.ChatID).
|
||||
Set(repository.Field("amount"), record.Amount).
|
||||
Set(repository.Field("currency"), record.Currency).
|
||||
Set(repository.Field(fieldTreasuryStatus), record.Status).
|
||||
Set(repository.Field("confirmedAt"), record.ConfirmedAt).
|
||||
Set(repository.Field("scheduledAt"), record.ScheduledAt).
|
||||
Set(repository.Field("executedAt"), record.ExecutedAt).
|
||||
Set(repository.Field("cancelledAt"), record.CancelledAt).
|
||||
Set(repository.Field(fieldTreasuryIdempotencyKey), record.IdempotencyKey).
|
||||
Set(repository.Field("ledgerReference"), record.LedgerReference).
|
||||
Set(repository.Field("errorMessage"), record.ErrorMessage).
|
||||
Set(repository.Field(fieldTreasuryActive), record.Active)
|
||||
if record.LedgerAccountCode != "" {
|
||||
patch = patch.Set(repository.Field("ledgerAccountCode"), record.LedgerAccountCode)
|
||||
} else {
|
||||
patch = patch.Unset(repository.Field("ledgerAccountCode"))
|
||||
}
|
||||
if !record.ConfirmedAt.IsZero() {
|
||||
patch = patch.Set(repository.Field("confirmedAt"), record.ConfirmedAt)
|
||||
} else {
|
||||
patch = patch.Unset(repository.Field("confirmedAt"))
|
||||
}
|
||||
if !record.ScheduledAt.IsZero() {
|
||||
patch = patch.Set(repository.Field("scheduledAt"), record.ScheduledAt)
|
||||
} else {
|
||||
patch = patch.Unset(repository.Field("scheduledAt"))
|
||||
}
|
||||
if !record.ExecutedAt.IsZero() {
|
||||
patch = patch.Set(repository.Field("executedAt"), record.ExecutedAt)
|
||||
} else {
|
||||
patch = patch.Unset(repository.Field("executedAt"))
|
||||
}
|
||||
if !record.CancelledAt.IsZero() {
|
||||
patch = patch.Set(repository.Field("cancelledAt"), record.CancelledAt)
|
||||
} else {
|
||||
patch = patch.Unset(repository.Field("cancelledAt"))
|
||||
}
|
||||
if record.LedgerReference != "" {
|
||||
patch = patch.Set(repository.Field("ledgerReference"), record.LedgerReference)
|
||||
} else {
|
||||
patch = patch.Unset(repository.Field("ledgerReference"))
|
||||
}
|
||||
if record.ErrorMessage != "" {
|
||||
patch = patch.Set(repository.Field("errorMessage"), record.ErrorMessage)
|
||||
} else {
|
||||
patch = patch.Unset(repository.Field("errorMessage"))
|
||||
}
|
||||
if _, err := t.repo.PatchMany(ctx, repository.Filter(fieldTreasuryRequestID, record.RequestID), patch); err != nil {
|
||||
if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.logger.Warn("Failed to update treasury request", zap.Error(err), zap.String("request_id", record.RequestID))
|
||||
|
||||
@@ -15,6 +15,7 @@ type Repository interface {
|
||||
TelegramConfirmations() TelegramConfirmationsStore
|
||||
PendingConfirmations() PendingConfirmationsStore
|
||||
TreasuryRequests() TreasuryRequestsStore
|
||||
TreasuryTelegramUsers() TreasuryTelegramUsersStore
|
||||
}
|
||||
|
||||
type PaymentsStore interface {
|
||||
@@ -46,3 +47,7 @@ type TreasuryRequestsStore interface {
|
||||
Update(ctx context.Context, record *model.TreasuryRequest) error
|
||||
ListByAccountAndStatuses(ctx context.Context, ledgerAccountID string, statuses []model.TreasuryRequestStatus, dayStart, dayEnd time.Time) ([]model.TreasuryRequest, error)
|
||||
}
|
||||
|
||||
type TreasuryTelegramUsersStore interface {
|
||||
FindByTelegramUserID(ctx context.Context, telegramUserID string) (*model.TreasuryTelegramUser, error)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
class OperationDocumentInfo {
|
||||
final String operationRef;
|
||||
class OperationDocumentRef {
|
||||
final String gatewayService;
|
||||
final String operationRef;
|
||||
|
||||
const OperationDocumentInfo({
|
||||
required this.operationRef,
|
||||
const OperationDocumentRef({
|
||||
required this.gatewayService,
|
||||
required this.operationRef,
|
||||
});
|
||||
}
|
||||
@@ -25,6 +25,7 @@ class PayoutRoutes {
|
||||
static const walletTopUp = 'payout-wallet-top-up';
|
||||
|
||||
static const paymentTypeQuery = 'paymentType';
|
||||
static const destinationLedgerAccountRefQuery = 'destinationLedgerAccountRef';
|
||||
static const reportPaymentIdQuery = 'paymentId';
|
||||
|
||||
static const dashboardPath = '/dashboard';
|
||||
@@ -40,7 +41,6 @@ class PayoutRoutes {
|
||||
static const editWalletPath = '/methods/edit';
|
||||
static const walletTopUpPath = '/wallet/top-up';
|
||||
|
||||
|
||||
static String nameFor(PayoutDestination destination) {
|
||||
switch (destination) {
|
||||
case PayoutDestination.dashboard:
|
||||
@@ -126,9 +126,13 @@ class PayoutRoutes {
|
||||
|
||||
static Map<String, String> buildQueryParameters({
|
||||
PaymentType? paymentType,
|
||||
String? destinationLedgerAccountRef,
|
||||
}) {
|
||||
final params = <String, String>{
|
||||
if (paymentType != null) paymentTypeQuery: paymentType.name,
|
||||
if (destinationLedgerAccountRef != null &&
|
||||
destinationLedgerAccountRef.trim().isNotEmpty)
|
||||
destinationLedgerAccountRefQuery: destinationLedgerAccountRef.trim(),
|
||||
};
|
||||
return params;
|
||||
}
|
||||
@@ -140,35 +144,44 @@ class PayoutRoutes {
|
||||
? null
|
||||
: PaymentType.values.firstWhereOrNull((type) => type.name == raw);
|
||||
|
||||
static String? destinationLedgerAccountRefFromState(GoRouterState state) =>
|
||||
destinationLedgerAccountRefFromRaw(
|
||||
state.uri.queryParameters[destinationLedgerAccountRefQuery],
|
||||
);
|
||||
|
||||
static String? destinationLedgerAccountRefFromRaw(String? raw) {
|
||||
final value = raw?.trim();
|
||||
if (value == null || value.isEmpty) return null;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
extension PayoutNavigation on BuildContext {
|
||||
void goToPayout(PayoutDestination destination) => goNamed(PayoutRoutes.nameFor(destination));
|
||||
void goToPayout(PayoutDestination destination) =>
|
||||
goNamed(PayoutRoutes.nameFor(destination));
|
||||
|
||||
void pushToPayout(PayoutDestination destination) => pushNamed(PayoutRoutes.nameFor(destination));
|
||||
void pushToPayout(PayoutDestination destination) =>
|
||||
pushNamed(PayoutRoutes.nameFor(destination));
|
||||
|
||||
void goToPayment({
|
||||
PaymentType? paymentType,
|
||||
}) =>
|
||||
goNamed(
|
||||
String? destinationLedgerAccountRef,
|
||||
}) => goNamed(
|
||||
PayoutRoutes.payment,
|
||||
queryParameters: PayoutRoutes.buildQueryParameters(
|
||||
paymentType: paymentType,
|
||||
destinationLedgerAccountRef: destinationLedgerAccountRef,
|
||||
),
|
||||
);
|
||||
|
||||
void goToReportPayment(String paymentId) => goNamed(
|
||||
PayoutRoutes.reportPayment,
|
||||
queryParameters: {
|
||||
PayoutRoutes.reportPaymentIdQuery: paymentId,
|
||||
},
|
||||
queryParameters: {PayoutRoutes.reportPaymentIdQuery: paymentId},
|
||||
);
|
||||
|
||||
void pushToReportPayment(String paymentId) => pushNamed(
|
||||
PayoutRoutes.reportPayment,
|
||||
queryParameters: {
|
||||
PayoutRoutes.reportPaymentIdQuery: paymentId,
|
||||
},
|
||||
queryParameters: {PayoutRoutes.reportPaymentIdQuery: paymentId},
|
||||
);
|
||||
|
||||
void pushToWalletTopUp() => pushNamed(PayoutRoutes.walletTopUp);
|
||||
|
||||
@@ -228,6 +228,7 @@ RouteBase payoutShellRoute() => ShellRoute(
|
||||
onGoToPaymentWithoutRecipient: (type) =>
|
||||
_startPayment(context, recipient: null, paymentType: type),
|
||||
onTopUp: (wallet) => _openWalletTopUp(context, wallet),
|
||||
onLedgerAddFunds: (account) => _openLedgerAddFunds(context, account),
|
||||
onWalletTap: (wallet) => _openWalletEdit(context, wallet),
|
||||
onLedgerTap: (account) => _openLedgerEdit(context, account),
|
||||
),
|
||||
@@ -306,6 +307,8 @@ RouteBase payoutShellRoute() => ShellRoute(
|
||||
child: PaymentPage(
|
||||
onBack: (_) => _popOrGo(context),
|
||||
initialPaymentType: PayoutRoutes.paymentTypeFromState(state),
|
||||
initialDestinationLedgerAccountRef:
|
||||
PayoutRoutes.destinationLedgerAccountRefFromState(state),
|
||||
fallbackDestination: fallbackDestination,
|
||||
),
|
||||
);
|
||||
@@ -395,6 +398,20 @@ void _openLedgerEdit(BuildContext context, LedgerAccount account) {
|
||||
context.pushToEditWallet();
|
||||
}
|
||||
|
||||
void _openLedgerAddFunds(BuildContext context, LedgerAccount account) {
|
||||
context.read<PaymentSourceController>().selectLedgerByRef(
|
||||
account.ledgerAccountRef,
|
||||
);
|
||||
context.read<RecipientsProvider>().setCurrentObject(null);
|
||||
context.pushNamed(
|
||||
PayoutRoutes.payment,
|
||||
queryParameters: PayoutRoutes.buildQueryParameters(
|
||||
paymentType: PaymentType.ledger,
|
||||
destinationLedgerAccountRef: account.ledgerAccountRef,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openWalletTopUp(BuildContext context, Wallet wallet) {
|
||||
context.read<WalletsController>().selectWallet(wallet);
|
||||
context.pushToWalletTopUp();
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/models/payment/operation_document.dart';
|
||||
import 'package:pshared/models/payment/execution_operation.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/provider/payment/payments.dart';
|
||||
import 'package:pweb/models/documents/operation.dart';
|
||||
|
||||
import 'package:pweb/utils/report/operations/document_rule.dart';
|
||||
|
||||
|
||||
class PaymentDetailsController extends ChangeNotifier {
|
||||
PaymentDetailsController({required String paymentId})
|
||||
: _paymentId = paymentId;
|
||||
@@ -20,7 +21,7 @@ class PaymentDetailsController extends ChangeNotifier {
|
||||
bool get isLoading => _payments?.isLoading ?? false;
|
||||
Exception? get error => _payments?.error;
|
||||
|
||||
OperationDocumentRequestModel? operationDocumentRequest(
|
||||
OperationDocumentRef? operationDocumentRequest(
|
||||
PaymentExecutionOperation operation,
|
||||
) {
|
||||
final current = _payment;
|
||||
@@ -33,7 +34,7 @@ class PaymentDetailsController extends ChangeNotifier {
|
||||
|
||||
if (!isOperationDocumentEligible(operation.code)) return null;
|
||||
|
||||
return OperationDocumentRequestModel(
|
||||
return OperationDocumentRef(
|
||||
gatewayService: gatewayService,
|
||||
operationRef: operationRef,
|
||||
);
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/controllers/payment/source.dart';
|
||||
import 'package:pshared/models/money.dart';
|
||||
import 'package:pshared/models/payment/asset.dart';
|
||||
import 'package:pshared/models/payment/chain_network.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/methods/ledger.dart';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/quote/status_type.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
|
||||
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||
import 'package:pweb/models/payment/multiple_payouts/state.dart';
|
||||
import 'package:pweb/providers/multiple_payouts.dart';
|
||||
import 'package:pweb/services/payments/csv_input.dart';
|
||||
|
||||
|
||||
class MultiplePayoutsController extends ChangeNotifier {
|
||||
final CsvInputService _csvInput;
|
||||
MultiplePayoutsProvider? _provider;
|
||||
PaymentSourceController? _sourceController;
|
||||
_PickState _pickState = _PickState.idle;
|
||||
Exception? _uiError;
|
||||
String? _lastSourceKey;
|
||||
|
||||
MultiplePayoutsController({required CsvInputService csvInput})
|
||||
: _csvInput = csvInput;
|
||||
@@ -37,6 +43,7 @@ class MultiplePayoutsController extends ChangeNotifier {
|
||||
_sourceController?.removeListener(_onSourceChanged);
|
||||
_sourceController = sourceController;
|
||||
_sourceController?.addListener(_onSourceChanged);
|
||||
_lastSourceKey = _currentSourceKey;
|
||||
shouldNotify = true;
|
||||
}
|
||||
if (shouldNotify) {
|
||||
@@ -60,16 +67,16 @@ class MultiplePayoutsController extends ChangeNotifier {
|
||||
_provider?.quoteStatusType ?? QuoteStatusType.missing;
|
||||
Duration? get quoteTimeLeft => _provider?.quoteTimeLeft;
|
||||
|
||||
bool get canSend => (_provider?.canSend ?? false) && _selectedWallet != null;
|
||||
bool get canSend => (_provider?.canSend ?? false) && _selectedSource != null;
|
||||
Money? get aggregateDebitAmount =>
|
||||
_provider?.aggregateDebitAmountFor(_selectedWallet);
|
||||
_provider?.aggregateDebitAmountForCurrency(_selectedSourceCurrencyCode);
|
||||
Money? get requestedSentAmount => _provider?.requestedSentAmount;
|
||||
Money? get aggregateSettlementAmount =>
|
||||
_provider?.aggregateSettlementAmountFor(_selectedWallet);
|
||||
Money? get aggregateSettlementAmount => _provider
|
||||
?.aggregateSettlementAmountForCurrency(_selectedSourceCurrencyCode);
|
||||
Money? get aggregateFeeAmount =>
|
||||
_provider?.aggregateFeeAmountFor(_selectedWallet);
|
||||
_provider?.aggregateFeeAmountForCurrency(_selectedSourceCurrencyCode);
|
||||
double? get aggregateFeePercent =>
|
||||
_provider?.aggregateFeePercentFor(_selectedWallet);
|
||||
_provider?.aggregateFeePercentForCurrency(_selectedSourceCurrencyCode);
|
||||
|
||||
Future<void> pickAndQuote() async {
|
||||
if (_pickState == _PickState.picking) return;
|
||||
@@ -84,15 +91,16 @@ class MultiplePayoutsController extends ChangeNotifier {
|
||||
try {
|
||||
final picked = await _csvInput.pickCsv();
|
||||
if (picked == null) return;
|
||||
final wallet = _selectedWallet;
|
||||
if (wallet == null) {
|
||||
_setUiError(StateError('Select source wallet first'));
|
||||
final source = _selectedSource;
|
||||
if (source == null) {
|
||||
_setUiError(StateError('Select source of funds first'));
|
||||
return;
|
||||
}
|
||||
await provider.quoteFromCsv(
|
||||
fileName: picked.name,
|
||||
content: picked.content,
|
||||
sourceWallet: wallet,
|
||||
sourceMethod: source.method,
|
||||
sourceCurrencyCode: source.currencyCode,
|
||||
);
|
||||
} catch (e) {
|
||||
_setUiError(e);
|
||||
@@ -131,10 +139,78 @@ class MultiplePayoutsController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _onSourceChanged() {
|
||||
final currentSourceKey = _currentSourceKey;
|
||||
final sourceChanged = currentSourceKey != _lastSourceKey;
|
||||
_lastSourceKey = currentSourceKey;
|
||||
if (sourceChanged) {
|
||||
unawaited(_requoteWithUploadedRows());
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Wallet? get _selectedWallet => _sourceController?.selectedWallet;
|
||||
String? get _selectedSourceCurrencyCode =>
|
||||
_sourceController?.selectedCurrencyCode;
|
||||
String? get _currentSourceKey {
|
||||
final source = _sourceController;
|
||||
if (source == null ||
|
||||
source.selectedType == null ||
|
||||
source.selectedRef == null) {
|
||||
return null;
|
||||
}
|
||||
return '${source.selectedType!.name}:${source.selectedRef!}';
|
||||
}
|
||||
|
||||
({PaymentMethodData method, String currencyCode})? get _selectedSource {
|
||||
final source = _sourceController;
|
||||
if (source == null) return null;
|
||||
|
||||
final currencyCode = source.selectedCurrencyCode;
|
||||
if (currencyCode == null || currencyCode.isEmpty) return null;
|
||||
|
||||
final wallet = source.selectedWallet;
|
||||
if (wallet != null) {
|
||||
final hasAsset = (wallet.tokenSymbol ?? '').isNotEmpty;
|
||||
final asset = hasAsset
|
||||
? PaymentAsset(
|
||||
chain: wallet.network ?? ChainNetwork.unspecified,
|
||||
tokenSymbol: wallet.tokenSymbol!,
|
||||
contractAddress: wallet.contractAddress,
|
||||
)
|
||||
: null;
|
||||
return (
|
||||
method: ManagedWalletPaymentMethod(
|
||||
managedWalletRef: wallet.id,
|
||||
asset: asset,
|
||||
),
|
||||
currencyCode: currencyCode,
|
||||
);
|
||||
}
|
||||
|
||||
final ledger = source.selectedLedgerAccount;
|
||||
if (ledger != null) {
|
||||
return (
|
||||
method: LedgerPaymentMethod(ledgerAccountRef: ledger.ledgerAccountRef),
|
||||
currencyCode: currencyCode,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _requoteWithUploadedRows() async {
|
||||
final provider = _provider;
|
||||
if (provider == null) return;
|
||||
if (provider.selectedFileName == null || provider.rows.isEmpty) return;
|
||||
|
||||
final source = _selectedSource;
|
||||
if (source == null) return;
|
||||
|
||||
_clearUiError(notify: false);
|
||||
await provider.requoteUploadedRows(
|
||||
sourceMethod: source.method,
|
||||
sourceCurrencyCode: source.currencyCode,
|
||||
);
|
||||
}
|
||||
|
||||
void _setUiError(Object error) {
|
||||
_uiError = error is Exception ? error : Exception(error.toString());
|
||||
|
||||
@@ -638,7 +638,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"noFee": "No fee",
|
||||
"noFee": "None",
|
||||
|
||||
"recipientWillReceive": "Recipient will receive: {amount}",
|
||||
"@recipientWillReceive": {
|
||||
|
||||
@@ -638,7 +638,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"noFee": "Нет комиссии",
|
||||
"noFee": "Без оплаты",
|
||||
|
||||
"recipientWillReceive": "Получатель получит: {amount}",
|
||||
"@recipientWillReceive": {
|
||||
|
||||
26
frontend/pweb/lib/models/dashboard/balance_item.dart
Normal file
26
frontend/pweb/lib/models/dashboard/balance_item.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
|
||||
sealed class BalanceItem {
|
||||
const BalanceItem();
|
||||
|
||||
const factory BalanceItem.wallet(Wallet wallet) = WalletBalanceItem;
|
||||
const factory BalanceItem.ledger(LedgerAccount account) = LedgerBalanceItem;
|
||||
const factory BalanceItem.addAction() = AddBalanceActionItem;
|
||||
}
|
||||
|
||||
final class WalletBalanceItem extends BalanceItem {
|
||||
final Wallet wallet;
|
||||
|
||||
const WalletBalanceItem(this.wallet);
|
||||
}
|
||||
|
||||
final class LedgerBalanceItem extends BalanceItem {
|
||||
final LedgerAccount account;
|
||||
|
||||
const LedgerBalanceItem(this.account);
|
||||
}
|
||||
|
||||
final class AddBalanceActionItem extends BalanceItem {
|
||||
const AddBalanceActionItem();
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
class OperationDocumentRequestModel {
|
||||
final String gatewayService;
|
||||
final String operationRef;
|
||||
|
||||
const OperationDocumentRequestModel({
|
||||
required this.gatewayService,
|
||||
required this.operationRef,
|
||||
});
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
enum PaymentState {
|
||||
success,
|
||||
failed,
|
||||
cancelled,
|
||||
processing,
|
||||
unknown,
|
||||
}
|
||||
|
||||
PaymentState paymentStateFromRaw(String? raw) {
|
||||
final trimmed = (raw ?? '').trim().toUpperCase();
|
||||
final normalized = trimmed.startsWith('PAYMENT_STATE_')
|
||||
? trimmed.substring('PAYMENT_STATE_'.length)
|
||||
: trimmed;
|
||||
|
||||
switch (normalized) {
|
||||
case 'SUCCESS':
|
||||
return PaymentState.success;
|
||||
case 'FAILED':
|
||||
return PaymentState.failed;
|
||||
case 'CANCELLED':
|
||||
return PaymentState.cancelled;
|
||||
case 'PROCESSING':
|
||||
return PaymentState.processing;
|
||||
default:
|
||||
return PaymentState.unknown;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
|
||||
|
||||
enum BalanceItemType { wallet, ledger, addAction }
|
||||
|
||||
class BalanceItem {
|
||||
final BalanceItemType type;
|
||||
final Wallet? wallet;
|
||||
final LedgerAccount? account;
|
||||
|
||||
const BalanceItem.wallet(this.wallet) : type = BalanceItemType.wallet, account = null;
|
||||
|
||||
const BalanceItem.ledger(this.account) : type = BalanceItemType.ledger, wallet = null;
|
||||
|
||||
const BalanceItem.addAction() : type = BalanceItemType.addAction, wallet = null, account = null;
|
||||
|
||||
bool get isWallet => type == BalanceItemType.wallet;
|
||||
bool get isLedger => type == BalanceItemType.ledger;
|
||||
bool get isAdd => type == BalanceItemType.addAction;
|
||||
}
|
||||
@@ -1,17 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/models/payment/chain_network.dart';
|
||||
import 'package:pshared/utils/l10n/chain.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add_funds.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
|
||||
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart';
|
||||
|
||||
|
||||
class WalletCard extends StatelessWidget {
|
||||
@@ -28,56 +19,10 @@ class WalletCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final networkLabel = (wallet.network == null || wallet.network == ChainNetwork.unspecified)
|
||||
? null
|
||||
: wallet.network!.localizedName(context);
|
||||
final symbol = wallet.tokenSymbol?.trim();
|
||||
|
||||
return Card(
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
elevation: WalletCardConfig.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||
onTap: onTap,
|
||||
child: SizedBox.expand(
|
||||
child: Padding(
|
||||
padding: WalletCardConfig.contentPadding,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BalanceHeader(
|
||||
title: wallet.name,
|
||||
subtitle: networkLabel,
|
||||
badge: (symbol == null || symbol.isEmpty) ? null : symbol,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
BalanceAmount(
|
||||
return BalanceSourceCard.wallet(
|
||||
wallet: wallet,
|
||||
onToggleMask: () {
|
||||
context.read<WalletsController>().toggleBalanceMask(wallet.id);
|
||||
},
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
WalletBalanceRefreshButton(
|
||||
walletRef: wallet.id,
|
||||
),
|
||||
BalanceAddFunds(onTopUp: onTopUp),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
onAddFunds: onTopUp,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,20 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
|
||||
import 'package:pweb/models/dashboard/balance_item.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/balance_item.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/card.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/ledger.dart';
|
||||
|
||||
|
||||
class BalanceCarousel extends StatefulWidget {
|
||||
final List<BalanceItem> items;
|
||||
final int currentIndex;
|
||||
final ValueChanged<int> onIndexChanged;
|
||||
final ValueChanged<Wallet> onTopUp;
|
||||
final ValueChanged<LedgerAccount> onLedgerAddFunds;
|
||||
final ValueChanged<Wallet> onWalletTap;
|
||||
final ValueChanged<LedgerAccount> onLedgerTap;
|
||||
|
||||
@@ -25,6 +27,7 @@ class BalanceCarousel extends StatefulWidget {
|
||||
required this.currentIndex,
|
||||
required this.onIndexChanged,
|
||||
required this.onTopUp,
|
||||
required this.onLedgerAddFunds,
|
||||
required this.onWalletTap,
|
||||
required this.onLedgerTap,
|
||||
});
|
||||
@@ -101,17 +104,18 @@ class _BalanceCarouselState extends State<BalanceCarousel> {
|
||||
itemCount: widget.items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = widget.items[index];
|
||||
final Widget card = switch (item.type) {
|
||||
BalanceItemType.wallet => WalletCard(
|
||||
wallet: item.wallet!,
|
||||
onTopUp: () => widget.onTopUp(item.wallet!),
|
||||
onTap: () => widget.onWalletTap(item.wallet!),
|
||||
final Widget card = switch (item) {
|
||||
WalletBalanceItem(:final wallet) => WalletCard(
|
||||
wallet: wallet,
|
||||
onTopUp: () => widget.onTopUp(wallet),
|
||||
onTap: () => widget.onWalletTap(wallet),
|
||||
),
|
||||
BalanceItemType.ledger => LedgerAccountCard(
|
||||
account: item.account!,
|
||||
onTap: () => widget.onLedgerTap(item.account!),
|
||||
LedgerBalanceItem(:final account) => LedgerAccountCard(
|
||||
account: account,
|
||||
onTap: () => widget.onLedgerTap(account),
|
||||
onAddFunds: () => widget.onLedgerAddFunds(account),
|
||||
),
|
||||
BalanceItemType.addAction => const AddBalanceCard(),
|
||||
AddBalanceActionItem() => const AddBalanceCard(),
|
||||
};
|
||||
|
||||
return Padding(
|
||||
|
||||
@@ -3,7 +3,8 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/provider/ledger.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/balance_item.dart';
|
||||
import 'package:pweb/models/dashboard/balance_item.dart';
|
||||
|
||||
|
||||
class BalanceCarouselController with ChangeNotifier {
|
||||
WalletsController? _walletsController;
|
||||
@@ -73,14 +74,19 @@ class BalanceCarouselController with ChangeNotifier {
|
||||
String? _currentWalletRef(List<BalanceItem> items, int index) {
|
||||
if (items.isEmpty || index < 0 || index >= items.length) return null;
|
||||
final current = items[index];
|
||||
if (!current.isWallet) return null;
|
||||
return current.wallet?.id;
|
||||
return switch (current) {
|
||||
WalletBalanceItem(:final wallet) => wallet.id,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
int? _walletIndexByRef(List<BalanceItem> items, String? walletRef) {
|
||||
if (walletRef == null || walletRef.isEmpty) return null;
|
||||
final idx = items.indexWhere(
|
||||
(item) => item.isWallet && item.wallet?.id == walletRef,
|
||||
(item) => switch (item) {
|
||||
WalletBalanceItem(:final wallet) => wallet.id == walletRef,
|
||||
_ => false,
|
||||
},
|
||||
);
|
||||
if (idx < 0) return null;
|
||||
return idx;
|
||||
@@ -97,17 +103,17 @@ class BalanceCarouselController with ChangeNotifier {
|
||||
for (var i = 0; i < left.length; i++) {
|
||||
final a = left[i];
|
||||
final b = right[i];
|
||||
if (a.type != b.type) return false;
|
||||
if (a.runtimeType != b.runtimeType) return false;
|
||||
if (_itemIdentity(a) != _itemIdentity(b)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
String _itemIdentity(BalanceItem item) => switch (item.type) {
|
||||
BalanceItemType.wallet => item.wallet?.id ?? '',
|
||||
BalanceItemType.ledger => item.account?.ledgerAccountRef ?? '',
|
||||
BalanceItemType.addAction => 'add',
|
||||
String _itemIdentity(BalanceItem item) => switch (item) {
|
||||
WalletBalanceItem(:final wallet) => wallet.id,
|
||||
LedgerBalanceItem(:final account) => account.ledgerAccountRef,
|
||||
AddBalanceActionItem() => 'add',
|
||||
};
|
||||
|
||||
void _syncSelectedWallet() {
|
||||
@@ -115,9 +121,8 @@ class BalanceCarouselController with ChangeNotifier {
|
||||
if (walletsController == null || _items.isEmpty) return;
|
||||
|
||||
final current = _items[_index];
|
||||
if (!current.isWallet || current.wallet == null) return;
|
||||
|
||||
final wallet = current.wallet!;
|
||||
if (current is! WalletBalanceItem) return;
|
||||
final wallet = current.wallet;
|
||||
if (walletsController.selectedWallet?.id == wallet.id) return;
|
||||
walletsController.selectWallet(wallet);
|
||||
}
|
||||
|
||||
@@ -1,133 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/ledger_accounts.dart';
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
import 'package:pshared/utils/money.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
|
||||
import 'package:pweb/widgets/refresh_balance/ledger.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart';
|
||||
|
||||
|
||||
class LedgerAccountCard extends StatelessWidget {
|
||||
final LedgerAccount account;
|
||||
final VoidCallback onAddFunds;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const LedgerAccountCard({super.key, required this.account, this.onTap});
|
||||
|
||||
String _formatBalance() {
|
||||
final money = account.balance?.balance;
|
||||
if (money == null) return '--';
|
||||
|
||||
final amount = parseMoneyAmount(money.amount, fallback: double.nan);
|
||||
if (amount.isNaN) {
|
||||
return '${money.amount} ${money.currency}';
|
||||
}
|
||||
|
||||
try {
|
||||
final currency = currencyStringToCode(money.currency);
|
||||
final symbol = currencyCodeToSymbol(currency);
|
||||
if (symbol.trim().isEmpty) {
|
||||
return '${amountToString(amount)} ${money.currency}';
|
||||
}
|
||||
return '${amountToString(amount)} $symbol';
|
||||
} catch (_) {
|
||||
return '${amountToString(amount)} ${money.currency}';
|
||||
}
|
||||
}
|
||||
|
||||
String _formatMaskedBalance() {
|
||||
final currency = account.currency.trim();
|
||||
if (currency.isEmpty) return '••••';
|
||||
try {
|
||||
final symbol = currencyCodeToSymbol(currencyStringToCode(currency));
|
||||
if (symbol.trim().isEmpty) {
|
||||
return '•••• $currency';
|
||||
}
|
||||
return '•••• $symbol';
|
||||
} catch (_) {
|
||||
return '•••• $currency';
|
||||
}
|
||||
}
|
||||
const LedgerAccountCard({
|
||||
super.key,
|
||||
required this.account,
|
||||
required this.onAddFunds,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final accountName = account.name.trim();
|
||||
final accountCode = account.accountCode.trim();
|
||||
final title = accountName.isNotEmpty ? accountName : loc.paymentTypeLedger;
|
||||
final subtitle = accountCode.isNotEmpty ? accountCode : null;
|
||||
final badge = account.currency.trim().isEmpty
|
||||
? null
|
||||
: account.currency.toUpperCase();
|
||||
|
||||
return Card(
|
||||
color: colorScheme.onSecondary,
|
||||
elevation: WalletCardConfig.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: WalletCardConfig.contentPadding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BalanceHeader(title: title, subtitle: subtitle, badge: badge),
|
||||
Row(
|
||||
children: [
|
||||
Consumer<LedgerBalanceMaskController>(
|
||||
builder: (context, controller, _) {
|
||||
final isMasked = controller.isBalanceMasked(
|
||||
account.ledgerAccountRef,
|
||||
);
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
isMasked
|
||||
? _formatMaskedBalance()
|
||||
: _formatBalance(),
|
||||
style: textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: () => controller.toggleBalanceMask(
|
||||
account.ledgerAccountRef,
|
||||
),
|
||||
child: Icon(
|
||||
isMasked
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
size: 24,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
LedgerBalanceRefreshButton(
|
||||
ledgerAccountRef: account.ledgerAccountRef,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
return BalanceSourceCard.ledger(
|
||||
account: account,
|
||||
onTap: onTap ?? () {},
|
||||
onAddFunds: onAddFunds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/ledger_accounts.dart';
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart';
|
||||
|
||||
|
||||
class LedgerBalanceAmount extends StatelessWidget {
|
||||
final LedgerAccount account;
|
||||
|
||||
const LedgerBalanceAmount({super.key, required this.account});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Consumer<LedgerBalanceMaskController>(
|
||||
builder: (context, controller, _) {
|
||||
final isMasked = controller.isBalanceMasked(account.ledgerAccountRef);
|
||||
final balance = isMasked
|
||||
? LedgerBalanceFormatter.formatMasked(account)
|
||||
: LedgerBalanceFormatter.format(account);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
balance,
|
||||
style: textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
controller.toggleBalanceMask(account.ledgerAccountRef);
|
||||
},
|
||||
child: Icon(
|
||||
isMasked ? Icons.visibility_off : Icons.visibility,
|
||||
size: 24,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/ledger.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class LedgerSourceActions extends StatelessWidget {
|
||||
final String ledgerAccountRef;
|
||||
final VoidCallback onAddFunds;
|
||||
|
||||
const LedgerSourceActions({
|
||||
super.key,
|
||||
required this.ledgerAccountRef,
|
||||
required this.onAddFunds,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ledgerProvider = context.watch<LedgerAccountsProvider>();
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final isBusy =
|
||||
ledgerProvider.isWalletRefreshing(ledgerAccountRef) ||
|
||||
ledgerProvider.isLoading;
|
||||
final hasTarget = ledgerProvider.accounts.any(
|
||||
(a) => a.ledgerAccountRef == ledgerAccountRef,
|
||||
);
|
||||
|
||||
return BalanceActionsBar(
|
||||
isRefreshBusy: isBusy,
|
||||
canRefresh: hasTarget,
|
||||
onRefresh: () {
|
||||
context.read<LedgerAccountsProvider>().refreshBalance(ledgerAccountRef);
|
||||
},
|
||||
onAddFunds: onAddFunds,
|
||||
refreshLabel: loc.refreshBalance,
|
||||
addFundsLabel: loc.addFunds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/payment/wallets.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class WalletSourceActions extends StatelessWidget {
|
||||
final String walletRef;
|
||||
final VoidCallback onAddFunds;
|
||||
|
||||
const WalletSourceActions({
|
||||
super.key,
|
||||
required this.walletRef,
|
||||
required this.onAddFunds,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final walletsProvider = context.watch<WalletsProvider>();
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final isBusy =
|
||||
walletsProvider.isWalletRefreshing(walletRef) ||
|
||||
walletsProvider.isLoading;
|
||||
final hasTarget = walletsProvider.wallets.any((w) => w.id == walletRef);
|
||||
|
||||
return BalanceActionsBar(
|
||||
isRefreshBusy: isBusy,
|
||||
canRefresh: hasTarget,
|
||||
onRefresh: () {
|
||||
context.read<WalletsProvider>().refreshBalance(walletRef);
|
||||
},
|
||||
onAddFunds: onAddFunds,
|
||||
refreshLabel: loc.refreshBalance,
|
||||
addFundsLabel: loc.addFunds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/models/payment/chain_network.dart';
|
||||
import 'package:pshared/models/payment/source_type.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/utils/l10n/chain.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/ledger_amount.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/source/actions/ledger.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/source/actions/wallet.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/source/card_layout.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class BalanceSourceCard extends StatelessWidget {
|
||||
final PaymentSourceType _type;
|
||||
final Wallet? _wallet;
|
||||
final LedgerAccount? _ledgerAccount;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onAddFunds;
|
||||
|
||||
const BalanceSourceCard.wallet({
|
||||
super.key,
|
||||
required Wallet wallet,
|
||||
required this.onTap,
|
||||
required this.onAddFunds,
|
||||
}) : _type = PaymentSourceType.wallet,
|
||||
_wallet = wallet,
|
||||
_ledgerAccount = null;
|
||||
|
||||
const BalanceSourceCard.ledger({
|
||||
super.key,
|
||||
required LedgerAccount account,
|
||||
required this.onTap,
|
||||
required this.onAddFunds,
|
||||
}) : _type = PaymentSourceType.ledger,
|
||||
_wallet = null,
|
||||
_ledgerAccount = account;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => switch (_type) {
|
||||
PaymentSourceType.wallet => _buildWalletCard(context, _wallet!),
|
||||
PaymentSourceType.ledger => _buildLedgerCard(context, _ledgerAccount!),
|
||||
};
|
||||
|
||||
Widget _buildWalletCard(BuildContext context, Wallet wallet) {
|
||||
final networkLabel =
|
||||
(wallet.network == null || wallet.network == ChainNetwork.unspecified)
|
||||
? null
|
||||
: wallet.network!.localizedName(context);
|
||||
final symbol = wallet.tokenSymbol?.trim();
|
||||
|
||||
return BalanceSourceCardLayout(
|
||||
title: wallet.name,
|
||||
subtitle: networkLabel,
|
||||
badge: (symbol == null || symbol.isEmpty) ? null : symbol,
|
||||
onTap: onTap,
|
||||
actions: WalletSourceActions(
|
||||
walletRef: wallet.id,
|
||||
onAddFunds: onAddFunds,
|
||||
),
|
||||
amount: BalanceAmount(
|
||||
wallet: wallet,
|
||||
onToggleMask: () {
|
||||
context.read<WalletsController>().toggleBalanceMask(wallet.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLedgerCard(BuildContext context, LedgerAccount account) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final accountName = account.name.trim();
|
||||
final accountCode = account.accountCode.trim();
|
||||
final title = accountName.isNotEmpty ? accountName : loc.paymentTypeLedger;
|
||||
final subtitle = accountCode.isNotEmpty ? accountCode : null;
|
||||
final badge = account.currency.trim().isEmpty
|
||||
? null
|
||||
: account.currency.toUpperCase();
|
||||
|
||||
return BalanceSourceCardLayout(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
badge: badge,
|
||||
onTap: onTap,
|
||||
actions: LedgerSourceActions(
|
||||
ledgerAccountRef: account.ledgerAccountRef,
|
||||
onAddFunds: onAddFunds,
|
||||
),
|
||||
amount: LedgerBalanceAmount(account: account),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
|
||||
|
||||
|
||||
class BalanceSourceCardLayout extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final String? badge;
|
||||
final Widget amount;
|
||||
final Widget actions;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const BalanceSourceCardLayout({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.badge,
|
||||
required this.amount,
|
||||
required this.actions,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Card(
|
||||
color: colorScheme.onSecondary,
|
||||
elevation: WalletCardConfig.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||
onTap: onTap,
|
||||
child: SizedBox.expand(
|
||||
child: Padding(
|
||||
padding: WalletCardConfig.contentPadding,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BalanceHeader(title: title, subtitle: subtitle, badge: badge),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(child: amount),
|
||||
const SizedBox(width: 12),
|
||||
actions,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,16 @@ import 'package:pweb/pages/dashboard/buttons/balance/controller.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class BalanceWidget extends StatelessWidget {
|
||||
final ValueChanged<Wallet> onTopUp;
|
||||
final ValueChanged<LedgerAccount> onLedgerAddFunds;
|
||||
final ValueChanged<Wallet> onWalletTap;
|
||||
final ValueChanged<LedgerAccount> onLedgerTap;
|
||||
|
||||
const BalanceWidget({
|
||||
super.key,
|
||||
required this.onTopUp,
|
||||
required this.onLedgerAddFunds,
|
||||
required this.onWalletTap,
|
||||
required this.onLedgerTap,
|
||||
});
|
||||
@@ -49,6 +50,7 @@ class BalanceWidget extends StatelessWidget {
|
||||
currentIndex: carousel.index,
|
||||
onIndexChanged: carousel.onPageChanged,
|
||||
onTopUp: onTopUp,
|
||||
onLedgerAddFunds: onLedgerAddFunds,
|
||||
onWalletTap: onWalletTap,
|
||||
onLedgerTap: onLedgerTap,
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ class DashboardPage extends StatefulWidget {
|
||||
final ValueChanged<Recipient> onRecipientSelected;
|
||||
final void Function(PaymentType type) onGoToPaymentWithoutRecipient;
|
||||
final ValueChanged<Wallet> onTopUp;
|
||||
final ValueChanged<LedgerAccount> onLedgerAddFunds;
|
||||
final ValueChanged<Wallet> onWalletTap;
|
||||
final ValueChanged<LedgerAccount> onLedgerTap;
|
||||
|
||||
@@ -35,6 +36,7 @@ class DashboardPage extends StatefulWidget {
|
||||
required this.onRecipientSelected,
|
||||
required this.onGoToPaymentWithoutRecipient,
|
||||
required this.onTopUp,
|
||||
required this.onLedgerAddFunds,
|
||||
required this.onWalletTap,
|
||||
required this.onLedgerTap,
|
||||
});
|
||||
@@ -90,6 +92,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||
BalanceWidgetProviders(
|
||||
child: BalanceWidget(
|
||||
onTopUp: widget.onTopUp,
|
||||
onLedgerAddFunds: widget.onLedgerAddFunds,
|
||||
onWalletTap: widget.onWalletTap,
|
||||
onLedgerTap: widget.onLedgerTap,
|
||||
),
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
|
||||
import 'package:pweb/controllers/payments/amount_field.dart';
|
||||
import 'package:pweb/models/payment/amount/mode.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/amount/mode/selector.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:pshared/provider/payment/multiple/quotation.dart';
|
||||
|
||||
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
|
||||
import 'package:pweb/controllers/payouts/payout_verification.dart';
|
||||
import 'package:pweb/utils/payment/payout_verification_flow.dart';
|
||||
import 'package:pweb/utils/payment/verification_flow.dart';
|
||||
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:pshared/utils/money.dart';
|
||||
|
||||
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
String moneyLabel(Money? money) {
|
||||
if (money == null) return 'N/A';
|
||||
@@ -12,10 +14,7 @@ String moneyLabel(Money? money) {
|
||||
if (amount.isNaN) return '${money.amount} ${money.currency}';
|
||||
try {
|
||||
return assetToString(
|
||||
Asset(
|
||||
currency: currencyStringToCode(money.currency),
|
||||
amount: amount,
|
||||
),
|
||||
Asset(currency: currencyStringToCode(money.currency), amount: amount),
|
||||
);
|
||||
} catch (_) {
|
||||
return '${money.amount} ${money.currency}';
|
||||
@@ -31,6 +30,8 @@ String sentAmountLabel(MultiplePayoutsController controller) {
|
||||
return moneyLabel(requested);
|
||||
}
|
||||
|
||||
String feeLabel(MultiplePayoutsController controller) {
|
||||
return moneyLabel(controller.aggregateFeeAmount);
|
||||
String feeLabel(MultiplePayoutsController controller, AppLocalizations l10n) {
|
||||
final fee = controller.aggregateFeeAmount;
|
||||
if (fee == null) return l10n.noFee;
|
||||
return moneyLabel(fee);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:pweb/controllers/payouts/multiple_payouts.dart';
|
||||
import 'package:pweb/models/dashboard/summary_values.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/summary/widget.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
class SourceQuoteSummary extends StatelessWidget {
|
||||
const SourceQuoteSummary({
|
||||
@@ -21,7 +21,7 @@ class SourceQuoteSummary extends StatelessWidget {
|
||||
return PaymentSummary(
|
||||
spacing: spacing,
|
||||
values: PaymentSummaryValues(
|
||||
fee: feeLabel(controller),
|
||||
fee: feeLabel(controller, AppLocalizations.of(context)!),
|
||||
recipientReceives: moneyLabel(controller.aggregateSettlementAmount),
|
||||
total: moneyLabel(controller.aggregateDebitAmount),
|
||||
),
|
||||
|
||||
@@ -23,7 +23,7 @@ class RecipientAvatar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textColor = Theme.of(context).colorScheme.onPrimary;
|
||||
final textColor = Theme.of(context).colorScheme.onSecondary;
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -31,7 +31,7 @@ class RecipientAvatar extends StatelessWidget {
|
||||
CircleAvatar(
|
||||
radius: avatarRadius,
|
||||
backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl!) : null,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryFixed,
|
||||
child: avatarUrl == null
|
||||
? Text(
|
||||
getInitials(name),
|
||||
|
||||
@@ -7,34 +7,32 @@ import 'package:pweb/pages/dashboard/payouts/single/address_book/avatar.dart';
|
||||
class ShortListAddressBookPayout extends StatelessWidget {
|
||||
final List<Recipient> recipients;
|
||||
final ValueChanged<Recipient> onSelected;
|
||||
final Widget? trailing;
|
||||
final Widget? leading;
|
||||
|
||||
const ShortListAddressBookPayout({
|
||||
super.key,
|
||||
required this.recipients,
|
||||
required this.onSelected,
|
||||
this.trailing,
|
||||
this.leading,
|
||||
});
|
||||
|
||||
static const double _avatarRadius = 20;
|
||||
static const double _avatarSize = 80;
|
||||
static const EdgeInsets _padding = EdgeInsets.symmetric(horizontal: 10, vertical: 8);
|
||||
static const EdgeInsets _padding = EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
);
|
||||
static const TextStyle _nameStyle = TextStyle(fontSize: 12);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final trailingWidget = trailing;
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children:
|
||||
recipients.map((recipient) {
|
||||
final leadingWidget = leading;
|
||||
final recipientItems = recipients.map((recipient) {
|
||||
return Padding(
|
||||
padding: _padding,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
hoverColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
hoverColor: Theme.of(context).colorScheme.onTertiary,
|
||||
onTap: () => onSelected(recipient),
|
||||
child: SizedBox(
|
||||
height: _avatarSize,
|
||||
@@ -49,12 +47,16 @@ class ShortListAddressBookPayout extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList()
|
||||
..addAll(
|
||||
trailingWidget == null
|
||||
? const []
|
||||
: [Padding(padding: _padding, child: trailingWidget)],
|
||||
),
|
||||
});
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
if (leadingWidget != null)
|
||||
Padding(padding: _padding, child: leadingWidget),
|
||||
...recipientItems,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,10 +21,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
class AddressBookPayout extends StatefulWidget {
|
||||
final ValueChanged<Recipient> onSelected;
|
||||
|
||||
const AddressBookPayout({
|
||||
super.key,
|
||||
required this.onSelected,
|
||||
});
|
||||
const AddressBookPayout({super.key, required this.onSelected});
|
||||
|
||||
@override
|
||||
State<AddressBookPayout> createState() => _AddressBookPayoutState();
|
||||
@@ -71,6 +68,7 @@ class _AddressBookPayoutState extends State<AddressBookPayout> {
|
||||
provider.setCurrentObject(null);
|
||||
context.pushNamed(PayoutRoutes.addRecipient);
|
||||
}
|
||||
|
||||
final filteredRecipients = filterRecipients(
|
||||
recipients: recipients,
|
||||
query: _query,
|
||||
@@ -81,16 +79,18 @@ class _AddressBookPayoutState extends State<AddressBookPayout> {
|
||||
}
|
||||
|
||||
if (provider.error != null) {
|
||||
return Center(child: Text(loc.notificationError(provider.error ?? loc.noErrorInformation)));
|
||||
return Center(
|
||||
child: Text(
|
||||
loc.notificationError(provider.error ?? loc.noErrorInformation),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: _isExpanded ? _expandedHeight : _collapsedHeight,
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(_cardMargin),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 4,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
child: Padding(
|
||||
@@ -121,7 +121,7 @@ class _AddressBookPayoutState extends State<AddressBookPayout> {
|
||||
: ShortListAddressBookPayout(
|
||||
recipients: recipients,
|
||||
onSelected: widget.onSelected,
|
||||
trailing: AddRecipientTile(
|
||||
leading: AddRecipientTile(
|
||||
label: loc.addRecipient,
|
||||
onTap: onAddRecipient,
|
||||
),
|
||||
|
||||
@@ -5,19 +5,21 @@ import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||
import 'package:pweb/controllers/payments/page_ui.dart';
|
||||
import 'package:pweb/pages/payout_page/send/page_handlers.dart';
|
||||
import 'package:pweb/utils/payment/page_handlers.dart';
|
||||
import 'package:pweb/pages/payout_page/send/page_view.dart';
|
||||
|
||||
|
||||
class PaymentPage extends StatefulWidget {
|
||||
final ValueChanged<Recipient?>? onBack;
|
||||
final PaymentType? initialPaymentType;
|
||||
final String? initialDestinationLedgerAccountRef;
|
||||
final PayoutDestination fallbackDestination;
|
||||
|
||||
const PaymentPage({
|
||||
super.key,
|
||||
this.onBack,
|
||||
this.initialPaymentType,
|
||||
this.initialDestinationLedgerAccountRef,
|
||||
this.fallbackDestination = PayoutDestination.dashboard,
|
||||
});
|
||||
|
||||
@@ -34,7 +36,11 @@ class _PaymentPageState extends State<PaymentPage> {
|
||||
_uiController = PaymentPageUiController();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => initializePaymentPage(context, widget.initialPaymentType),
|
||||
(_) => initializePaymentPage(
|
||||
context,
|
||||
widget.initialPaymentType,
|
||||
destinationLedgerAccountRef: widget.initialDestinationLedgerAccountRef,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/provider/payment/flow.dart';
|
||||
import 'package:pshared/provider/payment/quotation/quotation.dart';
|
||||
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
@@ -14,6 +15,7 @@ import 'package:pweb/controllers/payments/page_ui.dart';
|
||||
import 'package:pweb/controllers/payouts/payout_verification.dart';
|
||||
import 'package:pweb/models/state/control_state.dart';
|
||||
|
||||
|
||||
class PaymentPageView extends StatelessWidget {
|
||||
final PaymentPageUiController uiController;
|
||||
final ValueChanged<Recipient?>? onBack;
|
||||
@@ -47,6 +49,7 @@ class PaymentPageView extends StatelessWidget {
|
||||
final uiController = context.watch<PaymentPageUiController>();
|
||||
final methodsProvider = context.watch<PaymentMethodsProvider>();
|
||||
final recipientProvider = context.watch<RecipientsProvider>();
|
||||
final flowProvider = context.watch<PaymentFlowProvider>();
|
||||
final quotationProvider = context.watch<QuotationProvider>();
|
||||
final verificationController = context
|
||||
.watch<PayoutVerificationController>();
|
||||
@@ -58,10 +61,12 @@ class PaymentPageView extends StatelessWidget {
|
||||
recipients: recipientProvider.recipients,
|
||||
query: uiController.query,
|
||||
);
|
||||
final hasDestinationSelection =
|
||||
flowProvider.selectedPaymentData != null;
|
||||
final sendState =
|
||||
verificationController.isCooldownActiveFor(verificationContextKey)
|
||||
? ControlState.disabled
|
||||
: (recipient == null
|
||||
: (!hasDestinationSelection
|
||||
? ControlState.disabled
|
||||
: ControlState.enabled);
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/recipient/payment_method_draft.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart';
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart';
|
||||
import 'package:pweb/models/state/control_state.dart';
|
||||
import 'package:pweb/models/state/visibility.dart';
|
||||
import 'package:pweb/utils/dimensions.dart';
|
||||
|
||||
|
||||
class PaymentInfoManualDetailsSection extends StatelessWidget {
|
||||
final AppDimensions dimensions;
|
||||
final String title;
|
||||
final VisibilityState titleVisibility;
|
||||
final PaymentMethodData data;
|
||||
|
||||
const PaymentInfoManualDetailsSection({
|
||||
super.key,
|
||||
required this.dimensions,
|
||||
required this.title,
|
||||
required this.titleVisibility,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entry = RecipientMethodDraft(type: data.type, data: data);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PaymentInfoHeader(
|
||||
dimensions: dimensions,
|
||||
title: title,
|
||||
visibility: titleVisibility,
|
||||
),
|
||||
PaymentMethodPanel(
|
||||
selectedType: data.type,
|
||||
selectedIndex: 0,
|
||||
entries: [entry],
|
||||
onRemove: (_) {},
|
||||
onChanged: (_, ignored) {},
|
||||
editState: ControlState.disabled,
|
||||
deleteVisibility: VisibilityState.hidden,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:pshared/provider/payment/flow.dart';
|
||||
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_section.dart';
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_state.dart';
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/manual_details.dart';
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_methods.dart';
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_recipient.dart';
|
||||
import 'package:pweb/models/state/visibility.dart';
|
||||
@@ -35,8 +36,9 @@ class PaymentInfoSection extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final flowProvider = context.watch<PaymentFlowProvider>();
|
||||
final manualData = flowProvider.manualPaymentData;
|
||||
|
||||
if (!flowProvider.hasRecipient) {
|
||||
if (!flowProvider.hasRecipient && manualData == null) {
|
||||
return PaymentInfoNoRecipientSection(
|
||||
dimensions: dimensions,
|
||||
title: loc.paymentInfo,
|
||||
@@ -44,6 +46,15 @@ class PaymentInfoSection extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (!flowProvider.hasRecipient && manualData != null) {
|
||||
return PaymentInfoManualDetailsSection(
|
||||
dimensions: dimensions,
|
||||
title: loc.paymentInfo,
|
||||
titleVisibility: titleVisibility,
|
||||
data: manualData,
|
||||
);
|
||||
}
|
||||
|
||||
final methods = flowProvider.methodsForRecipient;
|
||||
final types = visiblePaymentTypes;
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class RecipientSection extends StatelessWidget {
|
||||
ShortListAddressBookPayout(
|
||||
recipients: recipientProvider.recipients,
|
||||
onSelected: onRecipientSelected,
|
||||
trailing: AddRecipientTile(
|
||||
leading: AddRecipientTile(
|
||||
label: loc.addRecipient,
|
||||
onTap: onAddRecipient,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/provider/payment/flow.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/section.dart';
|
||||
@@ -46,10 +49,15 @@ class PaymentRecipientDetailsCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final flowProvider = context.watch<PaymentFlowProvider>();
|
||||
final isRecipientSelectionLocked =
|
||||
!flowProvider.hasRecipient && flowProvider.manualPaymentData != null;
|
||||
|
||||
return PaymentSectionCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isRecipientSelectionLocked) ...[
|
||||
RecipientSection(
|
||||
recipient: recipient,
|
||||
dimensions: dimensions,
|
||||
@@ -64,6 +72,7 @@ class PaymentRecipientDetailsCard extends StatelessWidget {
|
||||
onAddRecipient: onAddRecipient,
|
||||
),
|
||||
SizedBox(height: dimensions.paddingMedium),
|
||||
],
|
||||
PaymentInfoSection(
|
||||
dimensions: dimensions,
|
||||
titleVisibility: VisibilityState.hidden,
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/payment/source.dart';
|
||||
import 'package:pshared/models/payment/source_type.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
|
||||
import 'package:pweb/app/router/payout_routes.dart';
|
||||
|
||||
@@ -17,11 +21,35 @@ class TopUpButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final source = context.watch<PaymentSourceController>();
|
||||
final canTopUp = source.selectedType == PaymentSourceType.wallet;
|
||||
final selectedType = source.selectedType;
|
||||
final selectedLedger = source.selectedLedgerAccount;
|
||||
final canTopUp =
|
||||
selectedType == PaymentSourceType.wallet ||
|
||||
(selectedType == PaymentSourceType.ledger && selectedLedger != null);
|
||||
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(shadowColor: null, elevation: 0),
|
||||
onPressed: canTopUp ? () => context.pushToWalletTopUp() : null,
|
||||
onPressed: !canTopUp
|
||||
? null
|
||||
: () {
|
||||
if (selectedType == PaymentSourceType.wallet) {
|
||||
context.pushToWalletTopUp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedType == PaymentSourceType.ledger &&
|
||||
selectedLedger != null) {
|
||||
context.read<RecipientsProvider>().setCurrentObject(null);
|
||||
context.pushNamed(
|
||||
PayoutRoutes.payment,
|
||||
queryParameters: PayoutRoutes.buildQueryParameters(
|
||||
paymentType: PaymentType.ledger,
|
||||
destinationLedgerAccountRef:
|
||||
selectedLedger.ledgerAccountRef,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(loc.topUpBalance),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:pshared/models/money.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/quote/quote.dart';
|
||||
import 'package:pshared/models/payment/quote/status_type.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/provider/payment/multiple/provider.dart';
|
||||
import 'package:pshared/provider/payment/multiple/quotation.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
@@ -13,8 +13,9 @@ import 'package:pshared/utils/payment/quote_helpers.dart';
|
||||
|
||||
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||
import 'package:pweb/models/payment/multiple_payouts/state.dart';
|
||||
import 'package:pweb/utils/payment/multiple_csv_parser.dart';
|
||||
import 'package:pweb/utils/payment/multiple_intent_builder.dart';
|
||||
import 'package:pweb/utils/payment/multiple/csv_parser.dart';
|
||||
import 'package:pweb/utils/payment/multiple/intent_builder.dart';
|
||||
|
||||
|
||||
class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
final MultipleCsvParser _csvParser;
|
||||
@@ -76,12 +77,12 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
return quoteRef != null && quoteRef.isNotEmpty;
|
||||
}
|
||||
|
||||
Money? aggregateDebitAmountFor(Wallet? sourceWallet) {
|
||||
Money? aggregateDebitAmountForCurrency(String? sourceCurrencyCode) {
|
||||
if (_rows.isEmpty) return null;
|
||||
final totals = aggregateMoneyByCurrency(
|
||||
_quoteItems().map((quote) => quote.amounts?.sourceDebitTotal),
|
||||
);
|
||||
return _moneyForSourceCurrency(totals, sourceWallet);
|
||||
return _moneyForSourceCurrency(totals, sourceCurrencyCode);
|
||||
}
|
||||
|
||||
Money? get requestedSentAmount {
|
||||
@@ -97,23 +98,23 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
return Money(amount: amountToString(total), currency: currency);
|
||||
}
|
||||
|
||||
Money? aggregateSettlementAmountFor(Wallet? sourceWallet) {
|
||||
Money? aggregateSettlementAmountForCurrency(String? sourceCurrencyCode) {
|
||||
if (_rows.isEmpty) return null;
|
||||
final totals = aggregateMoneyByCurrency(
|
||||
_quoteItems().map((quote) => quote.amounts?.destinationSettlement),
|
||||
);
|
||||
return _moneyForSourceCurrency(totals, sourceWallet);
|
||||
return _moneyForSourceCurrency(totals, sourceCurrencyCode);
|
||||
}
|
||||
|
||||
Money? aggregateFeeAmountFor(Wallet? sourceWallet) {
|
||||
Money? aggregateFeeAmountForCurrency(String? sourceCurrencyCode) {
|
||||
if (_rows.isEmpty) return null;
|
||||
final totals = aggregateMoneyByCurrency(_quoteItems().map(quoteFeeTotal));
|
||||
return _moneyForSourceCurrency(totals, sourceWallet);
|
||||
return _moneyForSourceCurrency(totals, sourceCurrencyCode);
|
||||
}
|
||||
|
||||
double? aggregateFeePercentFor(Wallet? sourceWallet) {
|
||||
final debit = aggregateDebitAmountFor(sourceWallet);
|
||||
final fee = aggregateFeeAmountFor(sourceWallet);
|
||||
double? aggregateFeePercentForCurrency(String? sourceCurrencyCode) {
|
||||
final debit = aggregateDebitAmountForCurrency(sourceCurrencyCode);
|
||||
final fee = aggregateFeeAmountForCurrency(sourceCurrencyCode);
|
||||
if (debit == null || fee == null) return null;
|
||||
|
||||
final debitValue = parseMoneyAmount(debit.amount, fallback: double.nan);
|
||||
@@ -126,7 +127,8 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
Future<void> quoteFromCsv({
|
||||
required String fileName,
|
||||
required String content,
|
||||
required Wallet sourceWallet,
|
||||
required PaymentMethodData sourceMethod,
|
||||
required String sourceCurrencyCode,
|
||||
}) async {
|
||||
if (isBusy) return;
|
||||
|
||||
@@ -144,18 +146,43 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
_sentCount = 0;
|
||||
|
||||
final rows = _csvParser.parseRows(content);
|
||||
final intents = _intentBuilder.buildIntents(sourceWallet, rows);
|
||||
await _quoteRows(
|
||||
quotation: quotation,
|
||||
fileName: fileName,
|
||||
rows: rows,
|
||||
sourceMethod: sourceMethod,
|
||||
sourceCurrencyCode: sourceCurrencyCode,
|
||||
);
|
||||
|
||||
_selectedFileName = fileName;
|
||||
_rows = rows;
|
||||
if (quotation.error != null) {
|
||||
_setErrorObject(quotation.error!);
|
||||
}
|
||||
} catch (e) {
|
||||
_setErrorObject(e);
|
||||
} finally {
|
||||
_setState(MultiplePayoutsState.idle);
|
||||
}
|
||||
}
|
||||
|
||||
await quotation.quotePayments(
|
||||
intents,
|
||||
metadata: <String, String>{
|
||||
'upload_filename': fileName,
|
||||
'upload_rows': rows.length.toString(),
|
||||
...?_uploadAmountMetadata(),
|
||||
},
|
||||
Future<void> requoteUploadedRows({
|
||||
required PaymentMethodData sourceMethod,
|
||||
required String sourceCurrencyCode,
|
||||
}) async {
|
||||
if (isBusy || _rows.isEmpty || _selectedFileName == null) return;
|
||||
final quotation = _quotation;
|
||||
if (quotation == null) return;
|
||||
|
||||
try {
|
||||
_setState(MultiplePayoutsState.quoting);
|
||||
_error = null;
|
||||
_sentCount = 0;
|
||||
|
||||
await _quoteRows(
|
||||
quotation: quotation,
|
||||
fileName: _selectedFileName!,
|
||||
rows: _rows,
|
||||
sourceMethod: sourceMethod,
|
||||
sourceCurrencyCode: sourceCurrencyCode,
|
||||
);
|
||||
|
||||
if (quotation.error != null) {
|
||||
@@ -254,13 +281,16 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
};
|
||||
}
|
||||
|
||||
Money? _moneyForSourceCurrency(List<Money>? values, Wallet? sourceWallet) {
|
||||
Money? _moneyForSourceCurrency(
|
||||
List<Money>? values,
|
||||
String? sourceCurrencyCode,
|
||||
) {
|
||||
if (values == null || values.isEmpty) return null;
|
||||
|
||||
if (sourceWallet != null) {
|
||||
final sourceCurrency = currencyCodeToString(sourceWallet.currency);
|
||||
if (sourceCurrencyCode != null && sourceCurrencyCode.isNotEmpty) {
|
||||
final sourceCurrency = sourceCurrencyCode.trim().toUpperCase();
|
||||
for (final value in values) {
|
||||
if (value.currency.toUpperCase() == sourceCurrency.toUpperCase()) {
|
||||
if (value.currency.toUpperCase() == sourceCurrency) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -272,6 +302,32 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
List<PaymentQuote> _quoteItems() =>
|
||||
_quotation?.quotation?.items ?? const <PaymentQuote>[];
|
||||
|
||||
Future<void> _quoteRows({
|
||||
required MultiQuotationProvider quotation,
|
||||
required String fileName,
|
||||
required List<CsvPayoutRow> rows,
|
||||
required PaymentMethodData sourceMethod,
|
||||
required String sourceCurrencyCode,
|
||||
}) async {
|
||||
final intents = _intentBuilder.buildIntents(
|
||||
sourceMethod: sourceMethod,
|
||||
sourceCurrency: sourceCurrencyCode,
|
||||
rows: rows,
|
||||
);
|
||||
|
||||
_selectedFileName = fileName;
|
||||
_rows = rows;
|
||||
|
||||
await quotation.quotePayments(
|
||||
intents,
|
||||
metadata: <String, String>{
|
||||
'upload_filename': fileName,
|
||||
'upload_rows': rows.length.toString(),
|
||||
...?_uploadAmountMetadata(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_quotation?.removeListener(_onQuotationChanged);
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import 'package:pshared/models/money.dart';
|
||||
import 'package:pshared/models/payment/asset.dart';
|
||||
import 'package:pshared/models/payment/chain_network.dart';
|
||||
import 'package:pshared/models/payment/fees/treatment.dart';
|
||||
import 'package:pshared/models/payment/intent.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/methods/data.dart';
|
||||
import 'package:pshared/models/payment/settlement_mode.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
import 'package:pshared/utils/payment/fx_helpers.dart';
|
||||
|
||||
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||
@@ -16,19 +12,11 @@ import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||
class MultipleIntentBuilder {
|
||||
static const String _currency = 'RUB';
|
||||
|
||||
List<PaymentIntent> buildIntents(
|
||||
Wallet sourceWallet,
|
||||
List<CsvPayoutRow> rows,
|
||||
) {
|
||||
final sourceCurrency = currencyCodeToString(sourceWallet.currency);
|
||||
final hasAsset = (sourceWallet.tokenSymbol ?? '').isNotEmpty;
|
||||
final sourceAsset = hasAsset
|
||||
? PaymentAsset(
|
||||
chain: sourceWallet.network ?? ChainNetwork.unspecified,
|
||||
tokenSymbol: sourceWallet.tokenSymbol!,
|
||||
contractAddress: sourceWallet.contractAddress,
|
||||
)
|
||||
: null;
|
||||
List<PaymentIntent> buildIntents({
|
||||
required PaymentMethodData sourceMethod,
|
||||
required String sourceCurrency,
|
||||
required List<CsvPayoutRow> rows,
|
||||
}) {
|
||||
final fxIntent = FxIntentHelper.buildSellBaseBuyQuote(
|
||||
baseCurrency: sourceCurrency,
|
||||
quoteCurrency: _currency,
|
||||
@@ -39,10 +27,7 @@ class MultipleIntentBuilder {
|
||||
final amount = Money(amount: row.amount, currency: _currency);
|
||||
return PaymentIntent(
|
||||
kind: PaymentKind.payout,
|
||||
source: ManagedWalletPaymentMethod(
|
||||
managedWalletRef: sourceWallet.id,
|
||||
asset: sourceAsset,
|
||||
),
|
||||
source: sourceMethod,
|
||||
destination: CardPaymentMethod(
|
||||
pan: row.pan,
|
||||
firstName: row.firstName,
|
||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/payment/methods/ledger.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/provider/payment/flow.dart';
|
||||
@@ -17,14 +18,30 @@ import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
|
||||
import 'package:pweb/controllers/payments/page.dart';
|
||||
import 'package:pweb/controllers/payments/page_ui.dart';
|
||||
import 'package:pweb/controllers/payouts/payout_verification.dart';
|
||||
import 'package:pweb/utils/payment/payout_verification_flow.dart';
|
||||
import 'package:pweb/utils/payment/verification_flow.dart';
|
||||
|
||||
|
||||
void initializePaymentPage(
|
||||
BuildContext context,
|
||||
PaymentType? initialPaymentType,
|
||||
) {
|
||||
PaymentType? initialPaymentType, {
|
||||
String? destinationLedgerAccountRef,
|
||||
}) {
|
||||
final flowProvider = context.read<PaymentFlowProvider>();
|
||||
final recipientsProvider = context.read<RecipientsProvider>();
|
||||
|
||||
flowProvider.setPreferredType(initialPaymentType);
|
||||
|
||||
final destinationRef = destinationLedgerAccountRef?.trim();
|
||||
if (destinationRef != null && destinationRef.isNotEmpty) {
|
||||
recipientsProvider.setCurrentObject(null);
|
||||
flowProvider.setPreferredType(PaymentType.ledger);
|
||||
flowProvider.setManualPaymentData(
|
||||
LedgerPaymentMethod(ledgerAccountRef: destinationRef),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
flowProvider.setManualPaymentData(null);
|
||||
}
|
||||
|
||||
void handleSearchChanged(PaymentPageUiController uiController, String query) {
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:pshared/models/payment/operation_document.dart';
|
||||
import 'package:pshared/models/payment/operation.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/state.dart';
|
||||
import 'package:pshared/models/payment/status.dart';
|
||||
import 'package:pshared/utils/money.dart';
|
||||
|
||||
import 'package:pweb/models/report/operation/document.dart';
|
||||
import 'package:pweb/utils/report/operations/document_rule.dart';
|
||||
|
||||
|
||||
OperationItem mapPaymentToOperation(Payment payment) {
|
||||
final debit = payment.lastQuote?.amounts?.sourceDebitTotal;
|
||||
final settlement = payment.lastQuote?.amounts?.destinationSettlement;
|
||||
@@ -55,7 +56,7 @@ OperationItem mapPaymentToOperation(Payment payment) {
|
||||
);
|
||||
}
|
||||
|
||||
OperationDocumentInfo? _resolveOperationDocument(Payment payment) {
|
||||
OperationDocumentRef? _resolveOperationDocument(Payment payment) {
|
||||
for (final operation in payment.operations) {
|
||||
final operationRef = operation.operationRef;
|
||||
final gatewayService = operation.gateway;
|
||||
@@ -64,7 +65,7 @@ OperationDocumentInfo? _resolveOperationDocument(Payment payment) {
|
||||
|
||||
if (!isOperationDocumentEligible(operation.code)) continue;
|
||||
|
||||
return OperationDocumentInfo(
|
||||
return OperationDocumentRef(
|
||||
operationRef: operationRef,
|
||||
gatewayService: gatewayService,
|
||||
);
|
||||
|
||||
112
frontend/pweb/lib/utils/report/source_filter.dart
Normal file
112
frontend/pweb/lib/utils/report/source_filter.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'package:pshared/models/payment/intent.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/methods/ledger.dart';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
import 'package:pshared/models/payment/methods/wallet.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/source_type.dart';
|
||||
|
||||
|
||||
bool paymentMatchesSource(
|
||||
Payment payment, {
|
||||
required PaymentSourceType sourceType,
|
||||
required String sourceRef,
|
||||
}) {
|
||||
final normalizedSourceRef = _normalize(sourceRef);
|
||||
if (normalizedSourceRef == null) return false;
|
||||
|
||||
final paymentSourceRef = _paymentSourceRef(payment, sourceType);
|
||||
return paymentSourceRef != null && paymentSourceRef == normalizedSourceRef;
|
||||
}
|
||||
|
||||
String? _paymentSourceRef(Payment payment, PaymentSourceType sourceType) {
|
||||
final fromIntent = _sourceRefFromIntent(payment.intent, sourceType);
|
||||
if (fromIntent != null) return fromIntent;
|
||||
return _sourceRefFromMetadata(payment.metadata, sourceType);
|
||||
}
|
||||
|
||||
String? _sourceRefFromIntent(
|
||||
PaymentIntent? intent,
|
||||
PaymentSourceType sourceType,
|
||||
) {
|
||||
final source = intent?.source;
|
||||
if (source == null) return null;
|
||||
|
||||
final fromIntentAttributes = _sourceRefFromMetadata(
|
||||
intent?.attributes,
|
||||
sourceType,
|
||||
);
|
||||
if (fromIntentAttributes != null) return fromIntentAttributes;
|
||||
|
||||
switch (sourceType) {
|
||||
case PaymentSourceType.wallet:
|
||||
return _walletSourceRef(source);
|
||||
case PaymentSourceType.ledger:
|
||||
return _ledgerSourceRef(source);
|
||||
}
|
||||
}
|
||||
|
||||
String? _walletSourceRef(PaymentMethodData source) {
|
||||
if (source is ManagedWalletPaymentMethod) {
|
||||
return _normalize(source.managedWalletRef) ??
|
||||
_sourceRefFromMetadata(source.metadata, PaymentSourceType.wallet);
|
||||
}
|
||||
if (source is WalletPaymentMethod) {
|
||||
return _normalize(source.walletId) ??
|
||||
_sourceRefFromMetadata(source.metadata, PaymentSourceType.wallet);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _ledgerSourceRef(PaymentMethodData source) {
|
||||
if (source is LedgerPaymentMethod) {
|
||||
return _normalize(source.ledgerAccountRef) ??
|
||||
_sourceRefFromMetadata(source.metadata, PaymentSourceType.ledger);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _sourceRefFromMetadata(
|
||||
Map<String, String>? metadata,
|
||||
PaymentSourceType sourceType,
|
||||
) {
|
||||
if (metadata == null || metadata.isEmpty) return null;
|
||||
|
||||
final keys = switch (sourceType) {
|
||||
PaymentSourceType.wallet => const <String>[
|
||||
'source_wallet_ref',
|
||||
'managed_wallet_ref',
|
||||
'wallet_ref',
|
||||
'wallet_id',
|
||||
'source_wallet_id',
|
||||
'source_wallet_user_id',
|
||||
'wallet_user_id',
|
||||
'wallet_user_ref',
|
||||
'wallet_number',
|
||||
'source_wallet_number',
|
||||
'source_managed_wallet_ref',
|
||||
'source_ref',
|
||||
],
|
||||
PaymentSourceType.ledger => const <String>[
|
||||
'source_ledger_account_ref',
|
||||
'ledger_account_ref',
|
||||
'source_account_code',
|
||||
'ledger_account_code',
|
||||
'account_code',
|
||||
'source_ref',
|
||||
],
|
||||
};
|
||||
|
||||
for (final key in keys) {
|
||||
final value = _normalize(metadata[key]);
|
||||
if (value != null) return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _normalize(String? value) {
|
||||
final normalized = value?.trim();
|
||||
if (normalized == null || normalized.isEmpty) return null;
|
||||
return normalized;
|
||||
}
|
||||
Reference in New Issue
Block a user