Skip to content

Commit b06dfe5

Browse files
author
jeffyanta
authored
Support tips in intents and chats (#103)
* Pull in latest protos * Support tip flag in intent data model * Support localization for new tip verbs * Support new Tips chat * Add basic happy path tip intent integration * Add basic edge case tests for tipping in SubmitIntent * Pull in latest protos * SubmitIntent now enforces Twitter tip address DNS * Include platform and username in memo for tip payment transaction * Move tip memo utility to transaction package * Update intent postgres store with additional tip metadata
1 parent b49115c commit b06dfe5

File tree

19 files changed

+665
-481
lines changed

19 files changed

+665
-481
lines changed

pkg/code/chat/chat.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const (
77
CodeTeamName = "Code Team"
88
KinPurchasesName = "Kin Purchases"
99
PaymentsName = "Payments" // Renamed to Web Payments on client
10+
TipsName = "Tips"
1011

1112
// Test chats used for unit/integration testing only
1213
TestCantMuteName = "TestCantMute"
@@ -39,6 +40,11 @@ var (
3940
CanMute: true,
4041
CanUnsubscribe: false,
4142
},
43+
TipsName: {
44+
TitleLocalizationKey: localization.ChatTitleTips,
45+
CanMute: true,
46+
CanUnsubscribe: false,
47+
},
4248

4349
TestCantMuteName: {
4450
TitleLocalizationKey: "n/a",

pkg/code/chat/message_cash_transactions.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import (
2020
// don't belong in the Cash Transactions chat will be ignored.
2121
//
2222
// Note: Tests covered in SubmitIntent history tests
23-
//
24-
// todo: How are we handling relationship account flows?
2523
func SendCashTransactionsExchangeMessage(ctx context.Context, data code_data.Provider, intentRecord *intent.Record) error {
2624
messageId := intentRecord.IntentId
2725

@@ -36,6 +34,9 @@ func SendCashTransactionsExchangeMessage(ctx context.Context, data code_data.Pro
3634
if intentRecord.SendPrivatePaymentMetadata.IsMicroPayment {
3735
// Micro payment messages exist in merchant domain-specific chats
3836
return nil
37+
} else if intentRecord.SendPrivatePaymentMetadata.IsTip {
38+
// Tip messages exist in a tip-specific chat
39+
return nil
3940
} else if intentRecord.SendPrivatePaymentMetadata.IsWithdrawal {
4041
if intentRecord.InitiatorOwnerAccount == intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount {
4142
// This is a top up for a public withdawal

pkg/code/chat/message_merchant.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ import (
1717
"github.com/code-payments/code-server/pkg/code/data/intent"
1818
)
1919

20-
type MessageWithOwner struct {
21-
Owner *common.Account
22-
Title string
23-
Message *chatpb.ChatMessage
24-
}
25-
2620
// SendMerchantExchangeMessage sends a message to the merchant's chat with
2721
// exchange data content related to the submitted intent. Intents that
2822
// don't belong in the merchant chat will be ignored. The set of chat messages

pkg/code/chat/message_tips.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package chat
2+
3+
import (
4+
"context"
5+
6+
"github.com/pkg/errors"
7+
8+
chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1"
9+
10+
"github.com/code-payments/code-server/pkg/code/common"
11+
code_data "github.com/code-payments/code-server/pkg/code/data"
12+
"github.com/code-payments/code-server/pkg/code/data/chat"
13+
"github.com/code-payments/code-server/pkg/code/data/intent"
14+
)
15+
16+
// SendTipsExchangeMessage sends a message to the Tips chat with exchange data
17+
// content related to the submitted intent. Intents that don't belong in the
18+
// Tips chat will be ignored.
19+
//
20+
// Note: Tests covered in SubmitIntent history tests
21+
func SendTipsExchangeMessage(ctx context.Context, data code_data.Provider, intentRecord *intent.Record) ([]*MessageWithOwner, error) {
22+
messageId := intentRecord.IntentId
23+
24+
exchangeData, ok := getExchangeDataFromIntent(intentRecord)
25+
if !ok {
26+
return nil, nil
27+
}
28+
29+
verbByMessageReceiver := make(map[string]chatpb.ExchangeDataContent_Verb)
30+
switch intentRecord.IntentType {
31+
case intent.SendPrivatePayment:
32+
if !intentRecord.SendPrivatePaymentMetadata.IsTip {
33+
// Not a tip
34+
return nil, nil
35+
}
36+
37+
verbByMessageReceiver[intentRecord.InitiatorOwnerAccount] = chatpb.ExchangeDataContent_SENT_TIP
38+
if len(intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount) > 0 {
39+
verbByMessageReceiver[intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount] = chatpb.ExchangeDataContent_RECEIVED_TIP
40+
}
41+
default:
42+
return nil, nil
43+
}
44+
45+
var messagesToPush []*MessageWithOwner
46+
for account, verb := range verbByMessageReceiver {
47+
receiver, err := common.NewAccountFromPublicKeyString(account)
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
content := []*chatpb.Content{
53+
{
54+
Type: &chatpb.Content_ExchangeData{
55+
ExchangeData: &chatpb.ExchangeDataContent{
56+
Verb: verb,
57+
ExchangeData: &chatpb.ExchangeDataContent_Exact{
58+
Exact: exchangeData,
59+
},
60+
},
61+
},
62+
},
63+
}
64+
protoMessage, err := newProtoChatMessage(messageId, content, intentRecord.CreatedAt)
65+
if err != nil {
66+
return nil, errors.Wrap(err, "error creating proto chat message")
67+
}
68+
69+
canPush, err := SendChatMessage(
70+
ctx,
71+
data,
72+
TipsName,
73+
chat.ChatTypeInternal,
74+
true,
75+
receiver,
76+
protoMessage,
77+
verb != chatpb.ExchangeDataContent_RECEIVED_TIP,
78+
)
79+
if err != nil && err != chat.ErrMessageAlreadyExists {
80+
return nil, errors.Wrap(err, "error persisting chat message")
81+
}
82+
83+
if canPush {
84+
messagesToPush = append(messagesToPush, &MessageWithOwner{
85+
Owner: receiver,
86+
Title: TipsName,
87+
Message: protoMessage,
88+
})
89+
}
90+
}
91+
92+
return messagesToPush, nil
93+
}

pkg/code/chat/util.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1"
1212
transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2"
1313

14+
"github.com/code-payments/code-server/pkg/code/common"
1415
code_data "github.com/code-payments/code-server/pkg/code/data"
1516
"github.com/code-payments/code-server/pkg/code/data/account"
1617
"github.com/code-payments/code-server/pkg/code/data/action"
@@ -19,6 +20,12 @@ import (
1920
"github.com/code-payments/code-server/pkg/kin"
2021
)
2122

23+
type MessageWithOwner struct {
24+
Owner *common.Account
25+
Title string
26+
Message *chatpb.ChatMessage
27+
}
28+
2229
func newProtoChatMessage(
2330
messageId string,
2431
content []*chatpb.Content,

pkg/code/data/intent/intent.go

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"errors"
55
"time"
66

7+
transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2"
8+
79
"github.com/code-payments/code-server/pkg/currency"
810
"github.com/code-payments/code-server/pkg/phone"
911
)
@@ -106,6 +108,15 @@ type SendPrivatePaymentMetadata struct {
106108
IsWithdrawal bool
107109
IsRemoteSend bool
108110
IsMicroPayment bool
111+
IsTip bool
112+
113+
// Set when IsTip = true
114+
TipMetadata *TipMetadata
115+
}
116+
117+
type TipMetadata struct {
118+
Platform transactionpb.TippedUser_Platform
119+
Username string
109120
}
110121

111122
type ReceivePaymentsPrivatelyMetadata struct {
@@ -545,6 +556,14 @@ func (m *OpenAccountsMetadata) Validate() error {
545556
}
546557

547558
func (m *SendPrivatePaymentMetadata) Clone() SendPrivatePaymentMetadata {
559+
var tipMetadata *TipMetadata
560+
if m.TipMetadata != nil {
561+
tipMetadata = &TipMetadata{
562+
Platform: m.TipMetadata.Platform,
563+
Username: m.TipMetadata.Username,
564+
}
565+
}
566+
548567
return SendPrivatePaymentMetadata{
549568
DestinationOwnerAccount: m.DestinationOwnerAccount,
550569
DestinationTokenAccount: m.DestinationTokenAccount,
@@ -554,13 +573,25 @@ func (m *SendPrivatePaymentMetadata) Clone() SendPrivatePaymentMetadata {
554573
ExchangeRate: m.ExchangeRate,
555574
NativeAmount: m.NativeAmount,
556575
UsdMarketValue: m.UsdMarketValue,
557-
IsWithdrawal: m.IsWithdrawal,
558-
IsRemoteSend: m.IsRemoteSend,
559-
IsMicroPayment: m.IsMicroPayment,
576+
577+
IsWithdrawal: m.IsWithdrawal,
578+
IsRemoteSend: m.IsRemoteSend,
579+
IsMicroPayment: m.IsMicroPayment,
580+
IsTip: m.IsTip,
581+
582+
TipMetadata: tipMetadata,
560583
}
561584
}
562585

563586
func (m *SendPrivatePaymentMetadata) CopyTo(dst *SendPrivatePaymentMetadata) {
587+
var tipMetadata *TipMetadata
588+
if m.TipMetadata != nil {
589+
tipMetadata = &TipMetadata{
590+
Platform: m.TipMetadata.Platform,
591+
Username: m.TipMetadata.Username,
592+
}
593+
}
594+
564595
dst.DestinationOwnerAccount = m.DestinationOwnerAccount
565596
dst.DestinationTokenAccount = m.DestinationTokenAccount
566597
dst.Quantity = m.Quantity
@@ -569,9 +600,13 @@ func (m *SendPrivatePaymentMetadata) CopyTo(dst *SendPrivatePaymentMetadata) {
569600
dst.ExchangeRate = m.ExchangeRate
570601
dst.NativeAmount = m.NativeAmount
571602
dst.UsdMarketValue = m.UsdMarketValue
603+
572604
dst.IsWithdrawal = m.IsWithdrawal
573605
dst.IsRemoteSend = m.IsRemoteSend
574606
dst.IsMicroPayment = m.IsMicroPayment
607+
dst.IsTip = m.IsTip
608+
609+
dst.TipMetadata = tipMetadata
575610
}
576611

577612
func (m *SendPrivatePaymentMetadata) Validate() error {
@@ -599,6 +634,22 @@ func (m *SendPrivatePaymentMetadata) Validate() error {
599634
return errors.New("usd market value cannot be zero")
600635
}
601636

637+
if m.IsTip {
638+
if m.TipMetadata == nil {
639+
return errors.New("tip metadata required for tips")
640+
}
641+
642+
if m.TipMetadata.Platform == transactionpb.TippedUser_UNKNOWN {
643+
return errors.New("tip platform is required")
644+
}
645+
646+
if len(m.TipMetadata.Username) == 0 {
647+
return errors.New("tip username is required")
648+
}
649+
} else if m.TipMetadata != nil {
650+
return errors.New("tip metadata can only be set for tips")
651+
}
652+
602653
return nil
603654
}
604655

0 commit comments

Comments
 (0)